Get data from downloaded cbz
This commit is contained in:
@@ -36,6 +36,17 @@
|
||||
</activity>
|
||||
|
||||
<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>
|
||||
|
||||
</manifest>
|
||||
@@ -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)
|
||||
|
||||
@@ -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 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<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),
|
||||
MINTMANGA("MintManga", "ru", MintMangaRepository::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
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.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)
|
||||
}
|
||||
}
|
||||
@@ -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() }
|
||||
fun File.takeIfReadable() = takeIf { it.exists() && it.canRead() }
|
||||
|
||||
fun ZipFile.readText(entry: ZipEntry) = getInputStream(entry).bufferedReader().use {
|
||||
it.readText()
|
||||
}
|
||||
14
app/src/main/java/org/koitharu/kotatsu/utils/ext/JsonExt.kt
Normal file
14
app/src/main/java/org/koitharu/kotatsu/utils/ext/JsonExt.kt
Normal file
@@ -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_next">Save this chapter and next</string>
|
||||
<string name="save_this_chapter">Save this chapter</string>
|
||||
<string name="no_saved_manga">No saved manga</string>
|
||||
</resources>
|
||||
6
app/src/main/res/xml/filepaths.xml
Normal file
6
app/src/main/res/xml/filepaths.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-files-path
|
||||
name="manga"
|
||||
path="/manga" />
|
||||
</paths>
|
||||
Reference in New Issue
Block a user