diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 91a8375aa..fecd4b629 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,6 +36,17 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index 9344a8670..30ef9a2ef 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -2,6 +2,9 @@ package org.koitharu.kotatsu import android.app.Application import androidx.room.Room +import coil.Coil +import coil.ImageLoader +import coil.util.CoilUtils import okhttp3.OkHttpClient import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidLogger @@ -18,6 +21,7 @@ class KotatsuApp : Application() { override fun onCreate() { super.onCreate() initKoin() + initCoil() } private fun initKoin() { @@ -50,6 +54,16 @@ class KotatsuApp : Application() { } } + private fun initCoil() { + Coil.setDefaultImageLoader(ImageLoader(applicationContext) { + okHttpClient { + okHttp() + .cache(CoilUtils.createDefaultCache(applicationContext)) + .build() + } + }) + } + private fun okHttp() = OkHttpClient.Builder() .connectTimeout(20, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/local/CbzFilter.kt b/app/src/main/java/org/koitharu/kotatsu/core/local/CbzFilter.kt new file mode 100644 index 000000000..5dda18a9d --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/local/CbzFilter.kt @@ -0,0 +1,9 @@ +package org.koitharu.kotatsu.core.local + +import java.io.File +import java.io.FilenameFilter + +class CbzFilter : FilenameFilter { + + override fun accept(dir: File, name: String) = name.endsWith(".cbz", ignoreCase = true) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt index feb6b0b16..d8b5ebcfc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/model/MangaSource.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.model import android.os.Parcelable import kotlinx.android.parcel.Parcelize +import org.koitharu.kotatsu.core.parser.LocalMangaRepository import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.site.MintMangaRepository import org.koitharu.kotatsu.core.parser.site.ReadmangaRepository @@ -9,7 +10,12 @@ import org.koitharu.kotatsu.core.parser.site.SelfMangaRepository @Suppress("SpellCheckingInspection") @Parcelize -enum class MangaSource(val title: String, val locale: String, val cls: Class): Parcelable { +enum class MangaSource( + val title: String, + val locale: String?, + val cls: Class +) : Parcelable { + LOCAL("Local", null, LocalMangaRepository::class.java), READMANGA_RU("ReadManga", "ru", ReadmangaRepository::class.java), MINTMANGA("MintManga", "ru", MintMangaRepository::class.java), SELFMANGA("SelfManga", "ru", SelfMangaRepository::class.java) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt new file mode 100644 index 000000000..efb25f9c8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/LocalMangaRepository.kt @@ -0,0 +1,94 @@ +package org.koitharu.kotatsu.core.parser + +import android.content.Context +import android.net.Uri +import androidx.core.net.toFile +import androidx.core.net.toUri +import org.koin.core.inject +import org.koitharu.kotatsu.core.local.CbzFilter +import org.koitharu.kotatsu.core.model.* +import org.koitharu.kotatsu.domain.MangaLoaderContext +import org.koitharu.kotatsu.domain.local.MangaIndex +import org.koitharu.kotatsu.domain.local.MangaZip +import org.koitharu.kotatsu.utils.ext.longHashCode +import org.koitharu.kotatsu.utils.ext.readText +import org.koitharu.kotatsu.utils.ext.safe +import java.io.File +import java.util.zip.ZipFile + +class LocalMangaRepository(loaderContext: MangaLoaderContext) : BaseMangaRepository(loaderContext) { + + private val context by loaderContext.inject() + + override suspend fun getList( + offset: Int, + query: String?, + sortOrder: SortOrder?, + tag: MangaTag? + ): List { + val files = context.getExternalFilesDirs("manga") + .flatMap { x -> x?.listFiles(CbzFilter())?.toList().orEmpty() } + return files.mapNotNull { x -> safe { getDetails(x) } } + } + + override suspend fun getDetails(manga: Manga) = manga + + override suspend fun getPages(chapter: MangaChapter): List { + val file = Uri.parse(chapter.url).toFile() + val zip = ZipFile(file) + val pattern = zip.getEntry(MangaZip.INDEX_ENTRY)?.let(zip::readText)?.let(::MangaIndex) + ?.getChapterNamesPattern(chapter) + val entries = if (pattern != null) { + zip.entries().asSequence() + .filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) } + } else { + zip.entries().asSequence().filter { x -> !x.isDirectory } + } + return entries.map { x -> + val uri = zipUri(file, x.name) + MangaPage( + id = uri.longHashCode(), + url = uri, + source = MangaSource.LOCAL + ) + }.toList() + } + + private fun getDetails(file: File): Manga { + val zip = ZipFile(file) + val fileUri = file.toUri().toString() + val entry = zip.getEntry(MangaZip.INDEX_ENTRY) + val index = entry?.let(zip::readText)?.let(::MangaIndex) + return index?.let { + it.getMangaInfo()?.let { x -> + x.copy( + source = MangaSource.LOCAL, + url = fileUri, + coverUrl = zipUri(file, it.getCoverEntry() ?: zip.entries().nextElement().name), + chapters = x.chapters?.map { c -> c.copy(url = fileUri) } + ) + } + } ?: run { + val title = file.nameWithoutExtension.replace("_", " ").capitalize() + Manga( + id = file.absolutePath.longHashCode(), + title = title, + url = fileUri, + source = MangaSource.LOCAL, + coverUrl = zipUri(file, zip.entries().nextElement().name), + chapters = listOf( + MangaChapter( + id = file.absolutePath.longHashCode(), + url = fileUri, + number = 1, + source = MangaSource.LOCAL, + name = title + ) + ) + ) + } + } + + private fun zipUri(file: File, entryName: String) = + Uri.fromParts("zip", file.path, entryName).toString() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaIndex.kt b/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaIndex.kt new file mode 100644 index 000000000..28a940009 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaIndex.kt @@ -0,0 +1,108 @@ +package org.koitharu.kotatsu.domain.local + +import org.json.JSONArray +import org.json.JSONObject +import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.core.model.Manga +import org.koitharu.kotatsu.core.model.MangaChapter +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.model.MangaTag +import org.koitharu.kotatsu.utils.ext.map +import org.koitharu.kotatsu.utils.ext.safe + +class MangaIndex(source: String?) { + + private val json: JSONObject = source?.let(::JSONObject) ?: JSONObject() + + fun setMangaInfo(manga: Manga) { + json.put("id", manga.id) + json.put("title", manga.title) + json.put("title_alt", manga.altTitle) + json.put("url", manga.url) + json.put("cover", manga.coverUrl) + json.put("description", manga.description) + json.put("rating", manga.rating) + json.put("source", manga.source.name) + json.put("cover_large", manga.largeCoverUrl) + json.put("tags", JSONArray().also { a -> + for (tag in manga.tags) { + val jo = JSONObject() + jo.put("key", tag.key) + jo.put("title", tag.title) + a.put(jo) + } + }) + json.put("chapters", JSONObject()) + json.put("app_id", BuildConfig.APPLICATION_ID) + json.put("app_version", BuildConfig.VERSION_CODE) + } + + fun getMangaInfo(): Manga? = if (json.length() == 0) null else safe { + val source = MangaSource.valueOf(json.getString("source")) + Manga( + id = json.getLong("id"), + title = json.getString("title"), + altTitle = json.getString("title_alt"), + url = json.getString("url"), + source = source, + rating = json.getDouble("rating").toFloat(), + coverUrl = json.getString("cover"), + description = json.getString("description"), + tags = json.getJSONArray("tags").map { x -> + MangaTag( + title = x.getString("title"), + key = x.getString("key"), + source = source + ) + }.toSet(), + chapters = getChapters(json.getJSONObject("chapters"), source) + ) + } + + fun getCoverEntry(): String? = json.optString("cover_entry") + + fun addChapter(chapter: MangaChapter) { + val chapters = json.getJSONObject("chapters") + if (!chapters.has(chapter.id.toString())) { + val jo = JSONObject() + jo.put("number", chapter.number) + jo.put("url", chapter.url) + jo.put("name", chapter.name) + jo.put("entries", "%03d\\d{3}".format(chapter.number)) + chapters.put(chapter.number.toString(), jo) + } + } + + fun setCoverEntry(name: String) { + json.put("cover_entry", name) + } + + fun getChapterNamesPattern(chapter: MangaChapter) = Regex( + json.getJSONObject("chapters") + .getJSONObject(chapter.id.toString()) + .getString("entries") + ) + + private fun getChapters(json: JSONObject, source: MangaSource): List { + val chapters = ArrayList(json.length()) + for (k in json.keys()) { + val v = json.getJSONObject(k) + chapters.add( + MangaChapter( + id = k.toLong(), + name = v.getString("name"), + url = v.getString("url"), + number = v.getInt("number"), + source = source + ) + ) + } + return chapters.sortedBy { it.number } + } + + override fun toString(): String = if (BuildConfig.DEBUG) { + json.toString(4) + } else { + json.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaZip.kt b/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaZip.kt index c4cac97b2..5d305f425 100644 --- a/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaZip.kt +++ b/app/src/main/java/org/koitharu/kotatsu/domain/local/MangaZip.kt @@ -1,12 +1,8 @@ package org.koitharu.kotatsu.domain.local import androidx.annotation.WorkerThread -import org.json.JSONArray -import org.json.JSONObject -import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaChapter -import org.koitharu.kotatsu.core.model.MangaPage import org.koitharu.kotatsu.utils.ext.sub import org.koitharu.kotatsu.utils.ext.takeIfReadable import org.koitharu.kotatsu.utils.ext.toFileName @@ -21,32 +17,11 @@ class MangaZip(private val file: File) { private val dir = file.parentFile?.sub(file.name + ".dir")?.takeIf { it.mkdir() } ?: throw RuntimeException("Cannot create temporary directory") - private lateinit var index: JSONObject + private val index = MangaIndex(dir.sub(INDEX_ENTRY).takeIfReadable()?.readText()) fun prepare(manga: Manga) { extract() - index = dir.sub("index.json").takeIfReadable()?.readText()?.let { JSONObject(it) } ?: JSONObject() - - index.put("id", manga.id) - index.put("title", manga.title) - index.put("title_alt", manga.altTitle) - index.put("url", manga.url) - index.put("cover", manga.coverUrl) - index.put("description", manga.description) - index.put("rating", manga.rating) - index.put("source", manga.source.name) - index.put("cover_large", manga.largeCoverUrl) - index.put("tags", JSONArray().also { a -> - for (tag in manga.tags) { - val jo = JSONObject() - jo.put("key", tag.key) - jo.put("title", tag.title) - a.put(jo) - } - }) - index.put("chapters", JSONObject()) - index.put("app_id", BuildConfig.APPLICATION_ID) - index.put("app_version", BuildConfig.VERSION_CODE) + index.setMangaInfo(manga) } fun cleanup() { @@ -54,7 +29,7 @@ class MangaZip(private val file: File) { } fun compress() { - dir.sub("index.json").writeText(index.toString(4)) + dir.sub(INDEX_ENTRY).writeText(index.toString()) ZipOutputStream(file.outputStream()).use { out -> for (file in dir.listFiles().orEmpty()) { val entry = ZipEntry(file.name) @@ -72,10 +47,10 @@ class MangaZip(private val file: File) { return } ZipInputStream(file.inputStream()).use { input -> - while(true) { + while (true) { val entry = input.nextEntry ?: return if (!entry.isDirectory) { - dir.sub(entry.name).outputStream().use { out-> + dir.sub(entry.name).outputStream().use { out -> input.copyTo(out) } } @@ -84,28 +59,36 @@ class MangaZip(private val file: File) { } } - fun addCover(file: File) { - val name = FILENAME_PATTERN.format(0, 0) + fun addCover(file: File, ext: String) { + val name = buildString { + append(FILENAME_PATTERN.format(0, 0)) + if (ext.isNotEmpty() && ext.length <= 4) { + append('.') + append(ext) + } + } file.copyTo(dir.sub(name), overwrite = true) + index.setCoverEntry(name) } - fun addPage(page: MangaPage, chapter: MangaChapter, file: File, pageNumber: Int) { - val name = FILENAME_PATTERN.format(chapter.number, pageNumber) - file.copyTo(dir.sub(name), overwrite = true) - val chapters = index.getJSONObject("chapters") - if (!chapters.has(chapter.number.toString())) { - val jo = JSONObject() - jo.put("id", chapter.id) - jo.put("url", chapter.url) - jo.put("name", chapter.name) - chapters.put(chapter.number.toString(), jo) + fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) { + val name = buildString { + append(FILENAME_PATTERN.format(chapter.number, pageNumber)) + if (ext.isNotEmpty() && ext.length <= 4) { + append('.') + append(ext) + } } + file.copyTo(dir.sub(name), overwrite = true) + index.addChapter(chapter) } companion object { private const val FILENAME_PATTERN = "%03d%03d" + const val INDEX_ENTRY = "index.json" + fun findInDir(root: File, manga: Manga): MangaZip { val name = manga.title.toFileName() + ".cbz" val file = File(root, name) diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/details/MangaDetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/ui/details/MangaDetailsActivity.kt index 8e638b370..8872d10b2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/details/MangaDetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/details/MangaDetailsActivity.kt @@ -2,15 +2,18 @@ package org.koitharu.kotatsu.ui.details import android.content.Context import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.Menu import android.view.MenuItem +import androidx.core.net.toFile import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.activity_details.* import moxy.ktx.moxyPresenter import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.MangaHistory +import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.ui.common.BaseActivity import org.koitharu.kotatsu.ui.download.DownloadService import org.koitharu.kotatsu.utils.ShareHelper @@ -36,6 +39,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView { override fun onMangaUpdated(manga: Manga) { this.manga = manga title = manga.title + invalidateOptionsMenu() } override fun onHistoryChanged(history: MangaHistory?) = Unit @@ -51,10 +55,20 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView { return super.onCreateOptionsMenu(menu) } + override fun onPrepareOptionsMenu(menu: Menu): Boolean { + menu.findItem(R.id.action_save).isEnabled = + manga?.source != null && manga?.source != MangaSource.LOCAL + return super.onPrepareOptionsMenu(menu) + } + override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { R.id.action_share -> { manga?.let { - ShareHelper.shareMangaLink(this, it) + if (it.source == MangaSource.LOCAL) { + ShareHelper.shareCbz(this, Uri.parse(it.url).toFile()) + } else { + ShareHelper.shareMangaLink(this, it) + } } true } diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt index 28ecc3a3b..73581fb8b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/download/DownloadService.kt @@ -2,6 +2,7 @@ package org.koitharu.kotatsu.ui.download import android.content.Context import android.content.Intent +import android.webkit.MimeTypeMap import androidx.core.content.ContextCompat import coil.Coil import coil.api.get @@ -65,8 +66,9 @@ class DownloadService : BaseService() { val data = if (manga.chapters == null) repo.getDetails(manga) else manga output = MangaZip.findInDir(destination, data) output.prepare(data) - downloadPage(data.largeCoverUrl ?: data.coverUrl, destination).let { file -> - output.addCover(file) + val coverUrl = data.largeCoverUrl ?: data.coverUrl + downloadPage(coverUrl, destination).let { file -> + output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl)) } val chapters = if (chaptersIds == null) { data.chapters.orEmpty() @@ -79,7 +81,12 @@ class DownloadService : BaseService() { for ((pageIndex, page) in pages.withIndex()) { val url = repo.getPageFullUrl(page) val file = cache[url] ?: downloadPage(url, destination) - output.addPage(page, chapter, file, pageIndex) + output.addPage( + chapter, + file, + pageIndex, + MimeTypeMap.getFileExtensionFromUrl(url) + ) withContext(Dispatchers.Main) { notification.setProgress( chapters.size, diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt index 9ef2b5ef5..66b901bb2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt @@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.ui.common.BaseActivity import org.koitharu.kotatsu.ui.main.list.favourites.FavouritesListFragment import org.koitharu.kotatsu.ui.main.list.history.HistoryListFragment +import org.koitharu.kotatsu.ui.main.list.local.LocalListFragment import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment import org.koitharu.kotatsu.utils.SearchHelper @@ -33,15 +34,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList navigationView.setNavigationItemSelectedListener(this) if (!supportFragmentManager.isStateSaved) { - navigationView.setCheckedItem(R.id.nav_history) - setPrimaryFragment(HistoryListFragment.newInstance()) + navigationView.setCheckedItem(R.id.nav_local_storage) + setPrimaryFragment(LocalListFragment.newInstance()) } } override fun onPostCreate(savedInstanceState: Bundle?) { super.onPostCreate(savedInstanceState) drawerToggle.syncState() - initSideMenu(MangaSource.values().asList()) + initSideMenu(MangaSource.values().asList() - MangaSource.LOCAL) } override fun onConfigurationChanged(newConfig: Configuration) { @@ -56,7 +57,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList } override fun onOptionsItemSelected(item: MenuItem): Boolean { - return drawerToggle.onOptionsItemSelected(item) || when(item.itemId) { + return drawerToggle.onOptionsItemSelected(item) || when (item.itemId) { else -> super.onOptionsItemSelected(item) } } @@ -68,7 +69,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList } else when (item.itemId) { R.id.nav_history -> setPrimaryFragment(HistoryListFragment.newInstance()) R.id.nav_favourites -> setPrimaryFragment(FavouritesListFragment.newInstance()) - R.id.nav_local_storage -> Unit + R.id.nav_local_storage -> setPrimaryFragment(LocalListFragment.newInstance()) else -> return false } drawer.closeDrawers() diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/local/LocalListFragment.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/local/LocalListFragment.kt new file mode 100644 index 000000000..a1bf63c93 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/local/LocalListFragment.kt @@ -0,0 +1,32 @@ +package org.koitharu.kotatsu.ui.main.list.local + +import kotlinx.android.synthetic.main.fragment_list.* +import moxy.ktx.moxyPresenter +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.ui.main.list.MangaListFragment +import java.io.File + +class LocalListFragment : MangaListFragment() { + + private val presenter by moxyPresenter(factory = ::LocalListPresenter) + + override fun onRequestMoreItems(offset: Int) { + if (offset == 0) { + presenter.loadList() + } + } + + override fun getTitle(): CharSequence? { + return getString(R.string.local_storage) + } + + override fun setUpEmptyListHolder() { + textView_holder.setText(R.string.no_saved_manga) + textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) + } + + companion object { + + fun newInstance() = LocalListFragment() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/list/local/LocalListPresenter.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/local/LocalListPresenter.kt new file mode 100644 index 000000000..a93cdd82b --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/list/local/LocalListPresenter.kt @@ -0,0 +1,43 @@ +package org.koitharu.kotatsu.ui.main.list.local + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import moxy.InjectViewState +import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.core.model.MangaSource +import org.koitharu.kotatsu.core.parser.LocalMangaRepository +import org.koitharu.kotatsu.domain.MangaProviderFactory +import org.koitharu.kotatsu.ui.common.BasePresenter +import org.koitharu.kotatsu.ui.main.list.MangaListView +import java.io.File + +@InjectViewState +class LocalListPresenter : BasePresenter>() { + + private lateinit var repository: LocalMangaRepository + + override fun onFirstViewAttach() { + repository = MangaProviderFactory.create(MangaSource.LOCAL) as LocalMangaRepository + super.onFirstViewAttach() + } + + fun loadList() { + launch { + viewState.onLoadingChanged(true) + try { + val list = withContext(Dispatchers.IO) { + repository.getList(0) + } + viewState.onListChanged(list) + } catch (e: Exception) { + if (BuildConfig.DEBUG) { + e.printStackTrace() + } + viewState.onError(e) + } finally { + viewState.onLoadingChanged(false) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt index 26f692e2c..31e526ccd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ShareHelper.kt @@ -2,8 +2,11 @@ package org.koitharu.kotatsu.utils import android.content.Context import android.content.Intent +import androidx.core.content.FileProvider +import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.Manga +import java.io.File object ShareHelper { @@ -19,4 +22,14 @@ object ShareHelper { val shareIntent = Intent.createChooser(intent, context.getString(R.string.share_s, manga.title)) context.startActivity(shareIntent) } + + @JvmStatic + fun shareCbz(context: Context, file: File) { + val uri = FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.files", file) + val intent = Intent(Intent.ACTION_SEND) + intent.setDataAndType(uri, context.contentResolver.getType(uri)) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + val shareIntent = Intent.createChooser(intent, context.getString(R.string.share_s, file.name)) + context.startActivity(shareIntent) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt index 93ee64752..00c8daf6a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/FileExt.kt @@ -1,7 +1,13 @@ package org.koitharu.kotatsu.utils.ext import java.io.File +import java.util.zip.ZipEntry +import java.util.zip.ZipFile fun File.sub(name: String) = File(this, name) -fun File.takeIfReadable() = takeIf { it.exists() && it.canRead() } \ No newline at end of file +fun File.takeIfReadable() = takeIf { it.exists() && it.canRead() } + +fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).bufferedReader().use { + it.readText() +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/JsonExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/JsonExt.kt new file mode 100644 index 000000000..46ba35987 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/JsonExt.kt @@ -0,0 +1,14 @@ +package org.koitharu.kotatsu.utils.ext + +import org.json.JSONArray +import org.json.JSONObject + +fun JSONArray.map(block: (JSONObject) -> T): List { + val len = length() + val result = ArrayList(len) + for(i in 0 until len) { + val jo = getJSONObject(i) + result.add(block(jo)) + } + return result +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 23d478a4c..145c389c7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -45,4 +45,5 @@ Save this chapter and prev. Save this chapter and next Save this chapter + No saved manga \ No newline at end of file diff --git a/app/src/main/res/xml/filepaths.xml b/app/src/main/res/xml/filepaths.xml new file mode 100644 index 000000000..b5c9b9e1a --- /dev/null +++ b/app/src/main/res/xml/filepaths.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file