Load local manga pages directly #552
This commit is contained in:
@@ -16,8 +16,8 @@ android {
|
|||||||
applicationId 'org.koitharu.kotatsu'
|
applicationId 'org.koitharu.kotatsu'
|
||||||
minSdk = 21
|
minSdk = 21
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 598
|
versionCode = 599
|
||||||
versionName = '6.3.1'
|
versionName = '6.3.2'
|
||||||
generatedDensities = []
|
generatedDensities = []
|
||||||
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
|
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
|
||||||
ksp {
|
ksp {
|
||||||
@@ -134,7 +134,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation 'io.coil-kt:coil-base:2.5.0'
|
implementation 'io.coil-kt:coil-base:2.5.0'
|
||||||
implementation 'io.coil-kt:coil-svg:2.5.0'
|
implementation 'io.coil-kt:coil-svg:2.5.0'
|
||||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:0fef1a47c9'
|
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:826d7b4512'
|
||||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||||
implementation 'io.noties.markwon:core:4.6.2'
|
implementation 'io.noties.markwon:core:4.6.2'
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import java.nio.file.attribute.BasicFileAttributes
|
|||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipFile
|
import java.util.zip.ZipFile
|
||||||
import kotlin.io.path.ExperimentalPathApi
|
import kotlin.io.path.ExperimentalPathApi
|
||||||
import kotlin.io.path.walk
|
|
||||||
import kotlin.io.path.readAttributes
|
import kotlin.io.path.readAttributes
|
||||||
|
import kotlin.io.path.walk
|
||||||
|
|
||||||
fun File.subdir(name: String) = File(this, name).also {
|
fun File.subdir(name: String) = File(this, name).also {
|
||||||
if (!it.exists()) it.mkdirs()
|
if (!it.exists()) it.mkdirs()
|
||||||
@@ -50,7 +50,7 @@ fun File.getStorageName(context: Context): String = runCatching {
|
|||||||
}
|
}
|
||||||
}.getOrNull() ?: context.getString(R.string.other_storage)
|
}.getOrNull() ?: context.getString(R.string.other_storage)
|
||||||
|
|
||||||
fun Uri.toFileOrNull() = if (scheme == "file") path?.let(::File) else null
|
fun Uri.toFileOrNull() = if (scheme == URI_SCHEME_FILE) path?.let(::File) else null
|
||||||
|
|
||||||
suspend fun File.deleteAwait() = withContext(Dispatchers.IO) {
|
suspend fun File.deleteAwait() = withContext(Dispatchers.IO) {
|
||||||
delete() || deleteRecursively()
|
delete() || deleteRecursively()
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package org.koitharu.kotatsu.core.util.ext
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.net.toFile
|
||||||
|
import okio.Source
|
||||||
|
import okio.source
|
||||||
|
import okio.use
|
||||||
|
import org.koitharu.kotatsu.local.data.util.withExtraCloseable
|
||||||
|
import java.io.File
|
||||||
|
import java.util.zip.ZipFile
|
||||||
|
|
||||||
|
const val URI_SCHEME_FILE = "file"
|
||||||
|
const val URI_SCHEME_ZIP = "file+zip"
|
||||||
|
|
||||||
|
fun Uri.exists(): Boolean = when (scheme) {
|
||||||
|
URI_SCHEME_FILE -> toFile().exists()
|
||||||
|
URI_SCHEME_ZIP -> {
|
||||||
|
val file = File(requireNotNull(schemeSpecificPart))
|
||||||
|
file.exists() && ZipFile(file).use { it.getEntry(fragment) != null }
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> unsupportedUri(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Uri.isTargetNotEmpty(): Boolean = when (scheme) {
|
||||||
|
URI_SCHEME_FILE -> toFile().isNotEmpty()
|
||||||
|
URI_SCHEME_ZIP -> {
|
||||||
|
val file = File(requireNotNull(schemeSpecificPart))
|
||||||
|
file.exists() && ZipFile(file).use { (it.getEntry(fragment)?.size ?: 0L) != 0L }
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> unsupportedUri(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Uri.source(): Source = when (scheme) {
|
||||||
|
URI_SCHEME_FILE -> toFile().source()
|
||||||
|
URI_SCHEME_ZIP -> {
|
||||||
|
val zip = ZipFile(schemeSpecificPart)
|
||||||
|
val entry = zip.getEntry(fragment)
|
||||||
|
zip.getInputStream(entry).source().withExtraCloseable(zip)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> unsupportedUri(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun File.toZipUri(entryName: String): Uri = Uri.parse("$URI_SCHEME_ZIP://$absolutePath#$entryName")
|
||||||
|
|
||||||
|
private fun unsupportedUri(uri: Uri): Nothing {
|
||||||
|
throw IllegalArgumentException("Bad uri $uri: only schemes $URI_SCHEME_FILE and $URI_SCHEME_ZIP are supported")
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.core.zip
|
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import androidx.collection.LruCache
|
|
||||||
import okhttp3.internal.closeQuietly
|
|
||||||
import okio.Source
|
|
||||||
import okio.source
|
|
||||||
import java.io.File
|
|
||||||
import java.util.zip.ZipFile
|
|
||||||
|
|
||||||
class ZipPool(maxSize: Int) : LruCache<String, ZipFile>(maxSize) {
|
|
||||||
|
|
||||||
override fun entryRemoved(evicted: Boolean, key: String, oldValue: ZipFile, newValue: ZipFile?) {
|
|
||||||
super.entryRemoved(evicted, key, oldValue, newValue)
|
|
||||||
oldValue.closeQuietly()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun create(key: String): ZipFile {
|
|
||||||
return ZipFile(File(key), ZipFile.OPEN_READ)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
@WorkerThread
|
|
||||||
operator fun get(uri: Uri): Source {
|
|
||||||
val zip = requireNotNull(get(uri.schemeSpecificPart)) {
|
|
||||||
"Cannot obtain zip by \"$uri\""
|
|
||||||
}
|
|
||||||
val entry = zip.getEntry(uri.fragment)
|
|
||||||
return zip.getInputStream(entry).source()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,6 @@ fun hasCbzExtension(string: String): Boolean {
|
|||||||
return isCbzExtension(ext)
|
return isCbzExtension(ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasCbzExtension(file: File) = isCbzExtension(file.name)
|
fun hasCbzExtension(file: File) = isCbzExtension(file.extension)
|
||||||
|
|
||||||
fun isCbzUri(uri: Uri) = isCbzExtension(uri.scheme)
|
fun isCbzUri(uri: Uri) = isCbzExtension(uri.scheme)
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import kotlinx.coroutines.runInterruptible
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_FILE
|
||||||
import org.koitharu.kotatsu.core.util.ext.computeSize
|
import org.koitharu.kotatsu.core.util.ext.computeSize
|
||||||
import org.koitharu.kotatsu.core.util.ext.getStorageName
|
import org.koitharu.kotatsu.core.util.ext.getStorageName
|
||||||
import org.koitharu.kotatsu.core.util.ext.resolveFile
|
import org.koitharu.kotatsu.core.util.ext.resolveFile
|
||||||
@@ -84,7 +85,7 @@ class LocalStorageManager @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun resolveUri(uri: Uri): File? = runInterruptible(Dispatchers.IO) {
|
suspend fun resolveUri(uri: Uri): File? = runInterruptible(Dispatchers.IO) {
|
||||||
if (uri.scheme == "file") {
|
if (uri.scheme == URI_SCHEME_FILE) {
|
||||||
uri.toFile()
|
uri.toFile()
|
||||||
} else {
|
} else {
|
||||||
uri.resolveFile(context)
|
uri.resolveFile(context)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.net.Uri
|
|||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
import androidx.collection.LongSparseArray
|
import androidx.collection.LongSparseArray
|
||||||
import androidx.collection.set
|
import androidx.collection.set
|
||||||
|
import androidx.core.net.toUri
|
||||||
import dagger.hilt.android.ActivityRetainedLifecycle
|
import dagger.hilt.android.ActivityRetainedLifecycle
|
||||||
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
@@ -33,15 +34,16 @@ import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
|
|||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
import org.koitharu.kotatsu.core.util.FileSize
|
import org.koitharu.kotatsu.core.util.FileSize
|
||||||
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
import org.koitharu.kotatsu.core.util.RetainedLifecycleCoroutineScope
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.URI_SCHEME_ZIP
|
||||||
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
import org.koitharu.kotatsu.core.util.ext.ensureSuccess
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.exists
|
||||||
import org.koitharu.kotatsu.core.util.ext.getCompletionResultOrNull
|
import org.koitharu.kotatsu.core.util.ext.getCompletionResultOrNull
|
||||||
import org.koitharu.kotatsu.core.util.ext.isNotEmpty
|
|
||||||
import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode
|
import org.koitharu.kotatsu.core.util.ext.isPowerSaveMode
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.isTargetNotEmpty
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
import org.koitharu.kotatsu.core.util.ext.ramAvailable
|
import org.koitharu.kotatsu.core.util.ext.ramAvailable
|
||||||
import org.koitharu.kotatsu.core.util.ext.withProgress
|
import org.koitharu.kotatsu.core.util.ext.withProgress
|
||||||
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
|
import org.koitharu.kotatsu.core.util.progress.ProgressDeferred
|
||||||
import org.koitharu.kotatsu.core.zip.ZipPool
|
|
||||||
import org.koitharu.kotatsu.local.data.PagesCache
|
import org.koitharu.kotatsu.local.data.PagesCache
|
||||||
import org.koitharu.kotatsu.local.data.isCbzUri
|
import org.koitharu.kotatsu.local.data.isCbzUri
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
@@ -71,13 +73,12 @@ class PageLoader @Inject constructor(
|
|||||||
|
|
||||||
val loaderScope = RetainedLifecycleCoroutineScope(lifecycle) + InternalErrorHandler() + Dispatchers.Default
|
val loaderScope = RetainedLifecycleCoroutineScope(lifecycle) + InternalErrorHandler() + Dispatchers.Default
|
||||||
|
|
||||||
private val tasks = LongSparseArray<ProgressDeferred<File, Float>>()
|
private val tasks = LongSparseArray<ProgressDeferred<Uri, Float>>()
|
||||||
private val semaphore = Semaphore(3)
|
private val semaphore = Semaphore(3)
|
||||||
private val convertLock = Mutex()
|
private val convertLock = Mutex()
|
||||||
private val prefetchLock = Mutex()
|
private val prefetchLock = Mutex()
|
||||||
private var repository: MangaRepository? = null
|
private var repository: MangaRepository? = null
|
||||||
private val prefetchQueue = LinkedList<MangaPage>()
|
private val prefetchQueue = LinkedList<MangaPage>()
|
||||||
private val zipPool = ZipPool(2)
|
|
||||||
private val counter = AtomicInteger(0)
|
private val counter = AtomicInteger(0)
|
||||||
private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive
|
private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive
|
||||||
|
|
||||||
@@ -85,7 +86,6 @@ class PageLoader @Inject constructor(
|
|||||||
synchronized(tasks) {
|
synchronized(tasks) {
|
||||||
tasks.clear()
|
tasks.clear()
|
||||||
}
|
}
|
||||||
zipPool.evictAll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isPrefetchApplicable(): Boolean {
|
fun isPrefetchApplicable(): Boolean {
|
||||||
@@ -113,7 +113,7 @@ class PageLoader @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadPageAsync(page: MangaPage, force: Boolean): ProgressDeferred<File, Float> {
|
fun loadPageAsync(page: MangaPage, force: Boolean): ProgressDeferred<Uri, Float> {
|
||||||
var task = tasks[page.id]?.takeIf { it.isValid() }
|
var task = tasks[page.id]?.takeIf { it.isValid() }
|
||||||
if (force) {
|
if (force) {
|
||||||
task?.cancel()
|
task?.cancel()
|
||||||
@@ -127,7 +127,7 @@ class PageLoader @Inject constructor(
|
|||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun loadPage(page: MangaPage, force: Boolean): File {
|
suspend fun loadPage(page: MangaPage, force: Boolean): Uri {
|
||||||
return loadPageAsync(page, force).await()
|
return loadPageAsync(page, force).await()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,11 +167,11 @@ class PageLoader @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadPageAsyncImpl(page: MangaPage, skipCache: Boolean): ProgressDeferred<File, Float> {
|
private fun loadPageAsyncImpl(page: MangaPage, skipCache: Boolean): ProgressDeferred<Uri, Float> {
|
||||||
val progress = MutableStateFlow(PROGRESS_UNDEFINED)
|
val progress = MutableStateFlow(PROGRESS_UNDEFINED)
|
||||||
val deferred = loaderScope.async {
|
val deferred = loaderScope.async {
|
||||||
if (!skipCache) {
|
if (!skipCache) {
|
||||||
cache.get(page.url)?.let { return@async it }
|
cache.get(page.url)?.let { return@async it.toUri() }
|
||||||
}
|
}
|
||||||
counter.incrementAndGet()
|
counter.incrementAndGet()
|
||||||
try {
|
try {
|
||||||
@@ -195,26 +195,20 @@ class PageLoader @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadPageImpl(page: MangaPage, progress: MutableStateFlow<Float>): File = semaphore.withPermit {
|
private suspend fun loadPageImpl(page: MangaPage, progress: MutableStateFlow<Float>): Uri = semaphore.withPermit {
|
||||||
val pageUrl = getPageUrl(page)
|
val pageUrl = getPageUrl(page)
|
||||||
check(pageUrl.isNotBlank()) { "Cannot obtain full image url" }
|
check(pageUrl.isNotBlank()) { "Cannot obtain full image url" }
|
||||||
val uri = Uri.parse(pageUrl)
|
val uri = Uri.parse(pageUrl)
|
||||||
return if (isCbzUri(uri)) {
|
return if (isCbzUri(uri)) {
|
||||||
runInterruptible(Dispatchers.IO) {
|
uri.buildUpon().scheme(URI_SCHEME_ZIP).build()
|
||||||
zipPool[uri]
|
|
||||||
}.use {
|
|
||||||
cache.put(pageUrl, it)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
val request = createPageRequest(page, pageUrl)
|
val request = createPageRequest(page, pageUrl)
|
||||||
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
|
imageProxyInterceptor.interceptPageRequest(request, okHttp).ensureSuccess().use { response ->
|
||||||
val body = checkNotNull(response.body) {
|
val body = checkNotNull(response.body) { "Null response body" }
|
||||||
"Null response"
|
|
||||||
}
|
|
||||||
body.withProgress(progress).use {
|
body.withProgress(progress).use {
|
||||||
cache.put(pageUrl, it.source())
|
cache.put(pageUrl, it.source())
|
||||||
}
|
}
|
||||||
}
|
}.toUri()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,9 +216,9 @@ class PageLoader @Inject constructor(
|
|||||||
return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES)
|
return context.ramAvailable <= FileSize.MEGABYTES.convert(PREFETCH_MIN_RAM_MB, FileSize.BYTES)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Deferred<File>.isValid(): Boolean {
|
private fun Deferred<Uri>.isValid(): Boolean {
|
||||||
return getCompletionResultOrNull()?.map { file ->
|
return getCompletionResultOrNull()?.map { uri ->
|
||||||
file.exists() && file.isNotEmpty()
|
uri.exists() && uri.isTargetNotEmpty()
|
||||||
}?.getOrDefault(false) ?: true
|
}?.getOrDefault(false) ?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
|
|||||||
import okio.IOException
|
import okio.IOException
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import okio.source
|
import org.koitharu.kotatsu.core.util.ext.source
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||||
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
|
import org.koitharu.kotatsu.parsers.util.toFileNameSafe
|
||||||
@@ -41,8 +42,8 @@ class PageSaveHelper @Inject constructor(
|
|||||||
saveLauncher: ActivityResultLauncher<String>,
|
saveLauncher: ActivityResultLauncher<String>,
|
||||||
): Uri {
|
): Uri {
|
||||||
val pageUrl = pageLoader.getPageUrl(page)
|
val pageUrl = pageLoader.getPageUrl(page)
|
||||||
val pageFile = pageLoader.loadPage(page, force = false)
|
val pageUri = pageLoader.loadPage(page, force = false)
|
||||||
val proposedName = getProposedFileName(pageUrl, pageFile)
|
val proposedName = getProposedFileName(pageUrl, pageUri)
|
||||||
val destination = withContext(Dispatchers.Main) {
|
val destination = withContext(Dispatchers.Main) {
|
||||||
suspendCancellableCoroutine { cont ->
|
suspendCancellableCoroutine { cont ->
|
||||||
continuation = cont
|
continuation = cont
|
||||||
@@ -54,7 +55,7 @@ class PageSaveHelper @Inject constructor(
|
|||||||
runInterruptible(Dispatchers.IO) {
|
runInterruptible(Dispatchers.IO) {
|
||||||
contentResolver.openOutputStream(destination)?.sink()?.buffer()
|
contentResolver.openOutputStream(destination)?.sink()?.buffer()
|
||||||
}?.use { output ->
|
}?.use { output ->
|
||||||
pageFile.source().use { input ->
|
pageUri.source().use { input ->
|
||||||
output.writeAllCancellable(input)
|
output.writeAllCancellable(input)
|
||||||
}
|
}
|
||||||
} ?: throw IOException("Output stream is null")
|
} ?: throw IOException("Output stream is null")
|
||||||
@@ -65,7 +66,7 @@ class PageSaveHelper @Inject constructor(
|
|||||||
resume(uri)
|
resume(uri)
|
||||||
} != null
|
} != null
|
||||||
|
|
||||||
private suspend fun getProposedFileName(url: String, file: File): String {
|
private suspend fun getProposedFileName(url: String, fileUri: Uri): String {
|
||||||
var name = if (url.startsWith("cbz://")) {
|
var name = if (url.startsWith("cbz://")) {
|
||||||
requireNotNull(url.toUri().fragment)
|
requireNotNull(url.toUri().fragment)
|
||||||
} else {
|
} else {
|
||||||
@@ -74,7 +75,7 @@ class PageSaveHelper @Inject constructor(
|
|||||||
var extension = name.substringAfterLast('.', "")
|
var extension = name.substringAfterLast('.', "")
|
||||||
name = name.substringBeforeLast('.')
|
name = name.substringBeforeLast('.')
|
||||||
if (extension.length !in 2..4) {
|
if (extension.length !in 2..4) {
|
||||||
val mimeType = getImageMimeType(file)
|
val mimeType = fileUri.toFileOrNull()?.let { file -> getImageMimeType(file) }
|
||||||
extension = if (mimeType != null) {
|
extension = if (mimeType != null) {
|
||||||
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
|
MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) ?: EXTENSION_FALLBACK
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.reader.ui.pager
|
package org.koitharu.kotatsu.reader.ui.pager
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.core.net.toFile
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
|
import com.davemorrissey.labs.subscaleview.DefaultOnImageEventListener
|
||||||
@@ -20,10 +21,10 @@ import kotlinx.coroutines.yield
|
|||||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||||
import org.koitharu.kotatsu.core.os.NetworkState
|
import org.koitharu.kotatsu.core.os.NetworkState
|
||||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||||
|
import org.koitharu.kotatsu.core.util.ext.toFileOrNull
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||||
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
import org.koitharu.kotatsu.reader.ui.config.ReaderSettings
|
||||||
import java.io.File
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
|
||||||
class PageHolderDelegate(
|
class PageHolderDelegate(
|
||||||
@@ -38,7 +39,7 @@ class PageHolderDelegate(
|
|||||||
var state = State.EMPTY
|
var state = State.EMPTY
|
||||||
private set
|
private set
|
||||||
private var job: Job? = null
|
private var job: Job? = null
|
||||||
private var file: File? = null
|
private var uri: Uri? = null
|
||||||
private var error: Throwable? = null
|
private var error: Throwable? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -87,15 +88,15 @@ class PageHolderDelegate(
|
|||||||
|
|
||||||
fun onRecycle() {
|
fun onRecycle() {
|
||||||
state = State.EMPTY
|
state = State.EMPTY
|
||||||
file = null
|
uri = null
|
||||||
error = null
|
error = null
|
||||||
job?.cancel()
|
job?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun reload() {
|
fun reload() {
|
||||||
if (state == State.SHOWN) {
|
if (state == State.SHOWN) {
|
||||||
file?.let {
|
uri?.let {
|
||||||
callback.onImageReady(it.toUri())
|
callback.onImageReady(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,10 +115,10 @@ class PageHolderDelegate(
|
|||||||
|
|
||||||
override fun onImageLoadError(e: Throwable) {
|
override fun onImageLoadError(e: Throwable) {
|
||||||
e.printStackTraceDebug()
|
e.printStackTraceDebug()
|
||||||
val file = this.file
|
val uri = this.uri
|
||||||
error = e
|
error = e
|
||||||
if (state == State.LOADED && e is IOException && file != null && file.exists()) {
|
if (state == State.LOADED && e is IOException && uri != null && uri.toFileOrNull()?.exists() != false) {
|
||||||
tryConvert(file, e)
|
tryConvert(uri, e)
|
||||||
} else {
|
} else {
|
||||||
state = State.ERROR
|
state = State.ERROR
|
||||||
callback.onError(e)
|
callback.onError(e)
|
||||||
@@ -131,12 +132,13 @@ class PageHolderDelegate(
|
|||||||
callback.onConfigChanged()
|
callback.onConfigChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun tryConvert(file: File, e: Exception) {
|
private fun tryConvert(uri: Uri, e: Exception) {
|
||||||
val prevJob = job
|
val prevJob = job
|
||||||
job = scope.launch {
|
job = scope.launch {
|
||||||
prevJob?.join()
|
prevJob?.join()
|
||||||
state = State.CONVERTING
|
state = State.CONVERTING
|
||||||
try {
|
try {
|
||||||
|
val file = uri.toFile()
|
||||||
loader.convertInPlace(file)
|
loader.convertInPlace(file)
|
||||||
state = State.CONVERTED
|
state = State.CONVERTED
|
||||||
callback.onImageReady(file.toUri())
|
callback.onImageReady(file.toUri())
|
||||||
@@ -157,14 +159,14 @@ class PageHolderDelegate(
|
|||||||
yield()
|
yield()
|
||||||
try {
|
try {
|
||||||
val task = loader.loadPageAsync(data, force)
|
val task = loader.loadPageAsync(data, force)
|
||||||
file = coroutineScope {
|
uri = coroutineScope {
|
||||||
val progressObserver = observeProgress(this, task.progressAsFlow())
|
val progressObserver = observeProgress(this, task.progressAsFlow())
|
||||||
val file = task.await()
|
val file = task.await()
|
||||||
progressObserver.cancelAndJoin()
|
progressObserver.cancelAndJoin()
|
||||||
file
|
file
|
||||||
}
|
}
|
||||||
state = State.LOADED
|
state = State.LOADED
|
||||||
callback.onImageReady(checkNotNull(file).toUri())
|
callback.onImageReady(checkNotNull(uri))
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
throw e
|
throw e
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
|
|||||||
Reference in New Issue
Block a user