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