Get data from downloaded cbz

pull/1/head
Koitharu 6 years ago
parent 5b858edc97
commit 4f02060d50

@ -36,6 +36,17 @@
</activity> </activity>
<service android:name=".ui.download.DownloadService" /> <service android:name=".ui.download.DownloadService" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.files"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</application> </application>
</manifest> </manifest>

@ -2,6 +2,9 @@ package org.koitharu.kotatsu
import android.app.Application import android.app.Application
import androidx.room.Room import androidx.room.Room
import coil.Coil
import coil.ImageLoader
import coil.util.CoilUtils
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
@ -18,6 +21,7 @@ class KotatsuApp : Application() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
initKoin() initKoin()
initCoil()
} }
private fun initKoin() { 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() private fun okHttp() = OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS) .connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS) .readTimeout(60, TimeUnit.SECONDS)

@ -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)
}

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.model
import android.os.Parcelable import android.os.Parcelable
import kotlinx.android.parcel.Parcelize 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.MangaRepository
import org.koitharu.kotatsu.core.parser.site.MintMangaRepository import org.koitharu.kotatsu.core.parser.site.MintMangaRepository
import org.koitharu.kotatsu.core.parser.site.ReadmangaRepository import org.koitharu.kotatsu.core.parser.site.ReadmangaRepository
@ -9,7 +10,12 @@ import org.koitharu.kotatsu.core.parser.site.SelfMangaRepository
@Suppress("SpellCheckingInspection") @Suppress("SpellCheckingInspection")
@Parcelize @Parcelize
enum class MangaSource(val title: String, val locale: String, val cls: Class<out MangaRepository>): Parcelable { enum class MangaSource(
val title: String,
val locale: String?,
val cls: Class<out MangaRepository>
) : Parcelable {
LOCAL("Local", null, LocalMangaRepository::class.java),
READMANGA_RU("ReadManga", "ru", ReadmangaRepository::class.java), READMANGA_RU("ReadManga", "ru", ReadmangaRepository::class.java),
MINTMANGA("MintManga", "ru", MintMangaRepository::class.java), MINTMANGA("MintManga", "ru", MintMangaRepository::class.java),
SELFMANGA("SelfManga", "ru", SelfMangaRepository::class.java) SELFMANGA("SelfManga", "ru", SelfMangaRepository::class.java)

@ -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<Context>()
override suspend fun getList(
offset: Int,
query: String?,
sortOrder: SortOrder?,
tag: MangaTag?
): List<Manga> {
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<MangaPage> {
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()
}

@ -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<MangaChapter> {
val chapters = ArrayList<MangaChapter>(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()
}
}

@ -1,12 +1,8 @@
package org.koitharu.kotatsu.domain.local package org.koitharu.kotatsu.domain.local
import androidx.annotation.WorkerThread 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.Manga
import org.koitharu.kotatsu.core.model.MangaChapter 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.sub
import org.koitharu.kotatsu.utils.ext.takeIfReadable import org.koitharu.kotatsu.utils.ext.takeIfReadable
import org.koitharu.kotatsu.utils.ext.toFileName 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() } private val dir = file.parentFile?.sub(file.name + ".dir")?.takeIf { it.mkdir() }
?: throw RuntimeException("Cannot create temporary directory") ?: 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) { fun prepare(manga: Manga) {
extract() extract()
index = dir.sub("index.json").takeIfReadable()?.readText()?.let { JSONObject(it) } ?: JSONObject() index.setMangaInfo(manga)
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)
} }
fun cleanup() { fun cleanup() {
@ -54,7 +29,7 @@ class MangaZip(private val file: File) {
} }
fun compress() { fun compress() {
dir.sub("index.json").writeText(index.toString(4)) dir.sub(INDEX_ENTRY).writeText(index.toString())
ZipOutputStream(file.outputStream()).use { out -> ZipOutputStream(file.outputStream()).use { out ->
for (file in dir.listFiles().orEmpty()) { for (file in dir.listFiles().orEmpty()) {
val entry = ZipEntry(file.name) val entry = ZipEntry(file.name)
@ -72,10 +47,10 @@ class MangaZip(private val file: File) {
return return
} }
ZipInputStream(file.inputStream()).use { input -> ZipInputStream(file.inputStream()).use { input ->
while(true) { while (true) {
val entry = input.nextEntry ?: return val entry = input.nextEntry ?: return
if (!entry.isDirectory) { if (!entry.isDirectory) {
dir.sub(entry.name).outputStream().use { out-> dir.sub(entry.name).outputStream().use { out ->
input.copyTo(out) input.copyTo(out)
} }
} }
@ -84,28 +59,36 @@ class MangaZip(private val file: File) {
} }
} }
fun addCover(file: File) { fun addCover(file: File, ext: String) {
val name = FILENAME_PATTERN.format(0, 0) 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) file.copyTo(dir.sub(name), overwrite = true)
index.setCoverEntry(name)
} }
fun addPage(page: MangaPage, chapter: MangaChapter, file: File, pageNumber: Int) { fun addPage(chapter: MangaChapter, file: File, pageNumber: Int, ext: String) {
val name = FILENAME_PATTERN.format(chapter.number, pageNumber) val name = buildString {
file.copyTo(dir.sub(name), overwrite = true) append(FILENAME_PATTERN.format(chapter.number, pageNumber))
val chapters = index.getJSONObject("chapters") if (ext.isNotEmpty() && ext.length <= 4) {
if (!chapters.has(chapter.number.toString())) { append('.')
val jo = JSONObject() append(ext)
jo.put("id", chapter.id) }
jo.put("url", chapter.url)
jo.put("name", chapter.name)
chapters.put(chapter.number.toString(), jo)
} }
file.copyTo(dir.sub(name), overwrite = true)
index.addChapter(chapter)
} }
companion object { companion object {
private const val FILENAME_PATTERN = "%03d%03d" private const val FILENAME_PATTERN = "%03d%03d"
const val INDEX_ENTRY = "index.json"
fun findInDir(root: File, manga: Manga): MangaZip { fun findInDir(root: File, manga: Manga): MangaZip {
val name = manga.title.toFileName() + ".cbz" val name = manga.title.toFileName() + ".cbz"
val file = File(root, name) val file = File(root, name)

@ -2,15 +2,18 @@ package org.koitharu.kotatsu.ui.details
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.core.net.toFile
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_details.* import kotlinx.android.synthetic.main.activity_details.*
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaHistory 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.common.BaseActivity
import org.koitharu.kotatsu.ui.download.DownloadService import org.koitharu.kotatsu.ui.download.DownloadService
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper
@ -36,6 +39,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
override fun onMangaUpdated(manga: Manga) { override fun onMangaUpdated(manga: Manga) {
this.manga = manga this.manga = manga
title = manga.title title = manga.title
invalidateOptionsMenu()
} }
override fun onHistoryChanged(history: MangaHistory?) = Unit override fun onHistoryChanged(history: MangaHistory?) = Unit
@ -51,10 +55,20 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView {
return super.onCreateOptionsMenu(menu) 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) { override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_share -> { R.id.action_share -> {
manga?.let { 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 true
} }

@ -2,6 +2,7 @@ package org.koitharu.kotatsu.ui.download
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.webkit.MimeTypeMap
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import coil.Coil import coil.Coil
import coil.api.get import coil.api.get
@ -65,8 +66,9 @@ class DownloadService : BaseService() {
val data = if (manga.chapters == null) repo.getDetails(manga) else manga val data = if (manga.chapters == null) repo.getDetails(manga) else manga
output = MangaZip.findInDir(destination, data) output = MangaZip.findInDir(destination, data)
output.prepare(data) output.prepare(data)
downloadPage(data.largeCoverUrl ?: data.coverUrl, destination).let { file -> val coverUrl = data.largeCoverUrl ?: data.coverUrl
output.addCover(file) downloadPage(coverUrl, destination).let { file ->
output.addCover(file, MimeTypeMap.getFileExtensionFromUrl(coverUrl))
} }
val chapters = if (chaptersIds == null) { val chapters = if (chaptersIds == null) {
data.chapters.orEmpty() data.chapters.orEmpty()
@ -79,7 +81,12 @@ class DownloadService : BaseService() {
for ((pageIndex, page) in pages.withIndex()) { for ((pageIndex, page) in pages.withIndex()) {
val url = repo.getPageFullUrl(page) val url = repo.getPageFullUrl(page)
val file = cache[url] ?: downloadPage(url, destination) val file = cache[url] ?: downloadPage(url, destination)
output.addPage(page, chapter, file, pageIndex) output.addPage(
chapter,
file,
pageIndex,
MimeTypeMap.getFileExtensionFromUrl(url)
)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
notification.setProgress( notification.setProgress(
chapters.size, chapters.size,

@ -13,6 +13,7 @@ import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.ui.common.BaseActivity import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.ui.main.list.favourites.FavouritesListFragment 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.history.HistoryListFragment
import org.koitharu.kotatsu.ui.main.list.local.LocalListFragment
import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment
import org.koitharu.kotatsu.utils.SearchHelper import org.koitharu.kotatsu.utils.SearchHelper
@ -33,15 +34,15 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
navigationView.setNavigationItemSelectedListener(this) navigationView.setNavigationItemSelectedListener(this)
if (!supportFragmentManager.isStateSaved) { if (!supportFragmentManager.isStateSaved) {
navigationView.setCheckedItem(R.id.nav_history) navigationView.setCheckedItem(R.id.nav_local_storage)
setPrimaryFragment(HistoryListFragment.newInstance()) setPrimaryFragment(LocalListFragment.newInstance())
} }
} }
override fun onPostCreate(savedInstanceState: Bundle?) { override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState) super.onPostCreate(savedInstanceState)
drawerToggle.syncState() drawerToggle.syncState()
initSideMenu(MangaSource.values().asList()) initSideMenu(MangaSource.values().asList() - MangaSource.LOCAL)
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
@ -56,7 +57,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return drawerToggle.onOptionsItemSelected(item) || when(item.itemId) { return drawerToggle.onOptionsItemSelected(item) || when (item.itemId) {
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
} }
@ -68,7 +69,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} else when (item.itemId) { } else when (item.itemId) {
R.id.nav_history -> setPrimaryFragment(HistoryListFragment.newInstance()) R.id.nav_history -> setPrimaryFragment(HistoryListFragment.newInstance())
R.id.nav_favourites -> setPrimaryFragment(FavouritesListFragment.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 else -> return false
} }
drawer.closeDrawers() drawer.closeDrawers()

@ -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<File>() {
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()
}
}

@ -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<MangaListView<File>>() {
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)
}
}
}
}

@ -2,8 +2,11 @@ package org.koitharu.kotatsu.utils
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.core.content.FileProvider
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import java.io.File
object ShareHelper { object ShareHelper {
@ -19,4 +22,14 @@ object ShareHelper {
val shareIntent = Intent.createChooser(intent, context.getString(R.string.share_s, manga.title)) val shareIntent = Intent.createChooser(intent, context.getString(R.string.share_s, manga.title))
context.startActivity(shareIntent) 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)
}
} }

@ -1,7 +1,13 @@
package org.koitharu.kotatsu.utils.ext package org.koitharu.kotatsu.utils.ext
import java.io.File import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
fun File.sub(name: String) = File(this, name) fun File.sub(name: String) = File(this, name)
fun File.takeIfReadable() = takeIf { it.exists() && it.canRead() } fun File.takeIfReadable() = takeIf { it.exists() && it.canRead() }
fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).bufferedReader().use {
it.readText()
}

@ -0,0 +1,14 @@
package org.koitharu.kotatsu.utils.ext
import org.json.JSONArray
import org.json.JSONObject
fun <T> JSONArray.map(block: (JSONObject) -> T): List<T> {
val len = length()
val result = ArrayList<T>(len)
for(i in 0 until len) {
val jo = getJSONObject(i)
result.add(block(jo))
}
return result
}

@ -45,4 +45,5 @@
<string name="save_this_chapter_and_prev">Save this chapter and prev.</string> <string name="save_this_chapter_and_prev">Save this chapter and prev.</string>
<string name="save_this_chapter_and_next">Save this chapter and next</string> <string name="save_this_chapter_and_next">Save this chapter and next</string>
<string name="save_this_chapter">Save this chapter</string> <string name="save_this_chapter">Save this chapter</string>
<string name="no_saved_manga">No saved manga</string>
</resources> </resources>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path
name="manga"
path="/manga" />
</paths>
Loading…
Cancel
Save