Refactor PageLoader (#1830)
* Cleanup PageLoader * Refactor ReaderScreen * Refactor EhPageLoader * Refine * Refine * Refine * Refine * Refine * Don't inline * Fix recompose * Merge PageLoader --------- Co-authored-by: Dude so hot <djohn@fbi.gov>
This commit is contained in:
1 parent
672b53d8a9
commit
0c00ea9292
12 files changed
+256
-359
No files matched your search
@@ -137,7 +137,7 @@ object Settings : DataStorePreferences(null) {
|
||||
var downloadPath by stringOrNullPref("image_path", null)
|
||||
var downloadQuery by stringOrNullPref("image_query", null)
|
||||
var downloadFragment by stringOrNullPref("image_fragment", null)
|
||||
var archivePasswds by stringSetOrNullPref("archive_passwds")
|
||||
var archivePasswds by stringSetPref("archive_passwds")
|
||||
var downloadDelay by intPref("download_delay_3", 1000)
|
||||
var multiThreadDownload by intPref("download_thread_2", 3)
|
||||
var preloadImage by intPref("preload_image_2", 5)
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
*/
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.collection.mutableIntObjectMapOf
|
||||
import arrow.fx.coroutines.autoCloseable
|
||||
import arrow.fx.coroutines.closeable
|
||||
import arrow.fx.coroutines.resourceScope
|
||||
import com.hippo.ehviewer.Settings.archivePasswds
|
||||
import com.hippo.ehviewer.image.ByteBufferSource
|
||||
import com.hippo.ehviewer.image.Image
|
||||
@@ -29,14 +32,11 @@ import com.hippo.ehviewer.jni.providePassword
|
||||
import com.hippo.ehviewer.jni.releaseByteBuffer
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.detectAds
|
||||
import com.hippo.ehviewer.util.displayPath
|
||||
import com.hippo.files.openFileDescriptor
|
||||
import com.hippo.files.toUri
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import java.nio.ByteBuffer
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -48,121 +48,97 @@ import okio.Path
|
||||
|
||||
typealias PasswdInvalidator = (String) -> Boolean
|
||||
typealias PasswdProvider = suspend (PasswdInvalidator) -> String
|
||||
val emptyPasswdProvider: PasswdProvider = { error("Managed Archive have password???") }
|
||||
|
||||
class ArchivePageLoader(
|
||||
private val file: Path,
|
||||
suspend fun <T> useArchivePageLoader(
|
||||
file: Path,
|
||||
gid: Long = 0,
|
||||
startPage: Int = 0,
|
||||
private val hasAds: Boolean = false,
|
||||
passwdProvider: PasswdProvider? = null,
|
||||
) : PageLoader2(gid, startPage) {
|
||||
private lateinit var pfd: ParcelFileDescriptor
|
||||
private val hostJob = async(start = CoroutineStart.LAZY) {
|
||||
logcat(DEBUG_TAG) { "Open archive ${file.toUri().displayPath}" }
|
||||
pfd = file.openFileDescriptor("r")
|
||||
val size = openArchive(pfd.fd, pfd.statSize, gid == 0L || file.name.endsWith(".zip"))
|
||||
if (size != 0 && passwdProvider != null && needPassword()) {
|
||||
archivePasswds?.forEach {
|
||||
it ?: return@forEach
|
||||
if (providePassword(it)) return@async size
|
||||
}
|
||||
hasAds: Boolean = false,
|
||||
passwdProvider: PasswdProvider = emptyPasswdProvider,
|
||||
block: suspend (PageLoader) -> T,
|
||||
) = coroutineScope {
|
||||
resourceScope {
|
||||
val pfd = closeable { file.openFileDescriptor("r") }
|
||||
val size = install(
|
||||
{ openArchive(pfd.fd, pfd.statSize, gid == 0L || file.name.endsWith(".zip")) },
|
||||
{ _, _ -> closeArchive() },
|
||||
)
|
||||
check(size != 0) { "Archive have no content!" }
|
||||
if (needPassword() && archivePasswds.filterNotNull().none { providePassword(it) }) {
|
||||
val toAdd = passwdProvider { providePassword(it) }
|
||||
archivePasswds = archivePasswds?.toMutableSet()?.apply { add(toAdd) } ?: setOf(toAdd)
|
||||
archivePasswds += toAdd
|
||||
}
|
||||
size
|
||||
}
|
||||
val loader = autoCloseable {
|
||||
object : PageLoader(gid, startPage) {
|
||||
private val jobs = mutableIntObjectMapOf<Job>()
|
||||
private val mutex = NamedMutex<Int>()
|
||||
private val semaphore = Semaphore(4)
|
||||
|
||||
override var size = 0
|
||||
override val title by lazy { FileUtils.getNameFromFilename(file.name)!! }
|
||||
|
||||
override fun start() {
|
||||
super.start()
|
||||
hostJob.start()
|
||||
}
|
||||
override fun getImageExtension(index: Int) = getExtension(index)
|
||||
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
closeArchive()
|
||||
pfd.close()
|
||||
logcat(DEBUG_TAG) { "Close archive ${file.toUri().displayPath} successfully!" }
|
||||
}
|
||||
override fun save(index: Int, file: Path) = runCatching {
|
||||
file.openFileDescriptor("w").use {
|
||||
extractToFd(index, it.fd)
|
||||
}
|
||||
}.getOrElse {
|
||||
logcat(it)
|
||||
false
|
||||
}
|
||||
|
||||
private val jobs = hashMapOf<Int, Job>()
|
||||
private val workerMutex = NamedMutex<Int>()
|
||||
private val semaphore = Semaphore(4)
|
||||
override val size = size
|
||||
|
||||
override fun onRequest(index: Int) {
|
||||
synchronized(jobs) {
|
||||
val current = jobs[index]
|
||||
if (current?.isActive != true) {
|
||||
jobs[index] = launch {
|
||||
semaphore.withPermit {
|
||||
workerMutex.withLock(index) {
|
||||
doRealWork(index)
|
||||
override fun prefetchPages(pages: List<Int>, bounds: Pair<Int, Int>) = Unit
|
||||
|
||||
override fun onRequest(index: Int) = synchronized(jobs) {
|
||||
val current = jobs[index]
|
||||
if (current?.isActive != true) {
|
||||
jobs[index] = launch {
|
||||
semaphore.withPermit {
|
||||
mutex.withLock(index) {
|
||||
doRealWork(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun doRealWork(index: Int) {
|
||||
val buffer = extractToByteBuffer(index) ?: return notifyPageFailed(index, null)
|
||||
check(buffer.isDirect)
|
||||
val src = object : ByteBufferSource {
|
||||
override val source: ByteBuffer = buffer
|
||||
|
||||
override fun close() {
|
||||
releaseByteBuffer(buffer)
|
||||
}
|
||||
}
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
src.close()
|
||||
throw it
|
||||
}
|
||||
val image = Image.decode(src, hasAds && detectAds(index, size)) ?: return notifyPageFailed(index, null)
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
image.recycle()
|
||||
throw it
|
||||
}
|
||||
notifyPageSucceed(index, image)
|
||||
}
|
||||
|
||||
override fun onForceRequest(index: Int, orgImg: Boolean) = onRequest(index)
|
||||
|
||||
override fun onCancelRequest(index: Int) {
|
||||
jobs[index]?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
loader.progressJob.join()
|
||||
block(loader)
|
||||
}
|
||||
|
||||
private suspend fun doRealWork(index: Int) {
|
||||
val buffer = extractToByteBuffer(index) ?: return notifyPageFailed(index, null)
|
||||
check(buffer.isDirect)
|
||||
val src = object : ByteBufferSource {
|
||||
override val source: ByteBuffer = buffer
|
||||
|
||||
override fun close() {
|
||||
releaseByteBuffer(buffer)
|
||||
}
|
||||
}
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
src.close()
|
||||
throw it
|
||||
}
|
||||
val image = Image.decode(src, hasAds && detectAds(index, size)) ?: return notifyPageFailed(index, null)
|
||||
runCatching {
|
||||
currentCoroutineContext().ensureActive()
|
||||
}.onFailure {
|
||||
image.recycle()
|
||||
throw it
|
||||
}
|
||||
notifyPageSucceed(index, image)
|
||||
}
|
||||
|
||||
override fun onForceRequest(index: Int, orgImg: Boolean) {
|
||||
onRequest(index)
|
||||
}
|
||||
|
||||
override suspend fun awaitReady(): Boolean {
|
||||
size = hostJob.await()
|
||||
return super.awaitReady() && isReady
|
||||
}
|
||||
|
||||
override val isReady: Boolean
|
||||
get() = size != 0
|
||||
|
||||
override fun onCancelRequest(index: Int) {
|
||||
jobs[index]?.cancel()
|
||||
}
|
||||
|
||||
override val title by lazy {
|
||||
FileUtils.getNameFromFilename(file.name)!!
|
||||
}
|
||||
|
||||
override fun getImageExtension(index: Int): String = getExtension(index)
|
||||
|
||||
override fun save(index: Int, file: Path): Boolean = runCatching {
|
||||
file.openFileDescriptor("w").use {
|
||||
extractToFd(index, it.fd)
|
||||
}
|
||||
}.getOrElse {
|
||||
logcat(it)
|
||||
false
|
||||
}
|
||||
|
||||
override fun prefetchPages(pages: List<Int>, bounds: Pair<Int, Int>) = Unit
|
||||
}
|
||||
|
||||
private const val DEBUG_TAG = "ArchivePageLoader"
|
||||
@@ -1,20 +1,7 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import arrow.fx.coroutines.autoCloseable
|
||||
import arrow.fx.coroutines.resourceScope
|
||||
import com.hippo.ehviewer.client.EhUtils
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.image.Image
|
||||
@@ -24,88 +11,62 @@ import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen
|
||||
import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener
|
||||
import okio.Path
|
||||
|
||||
class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) :
|
||||
PageLoader2(mGalleryInfo.gid, startPage),
|
||||
OnSpiderListener {
|
||||
private lateinit var mSpiderQueen: SpiderQueen
|
||||
override fun start() {
|
||||
super.start()
|
||||
if (!::mSpiderQueen.isInitialized) {
|
||||
mSpiderQueen = obtainSpiderQueen(mGalleryInfo, SpiderQueen.MODE_READ)
|
||||
mSpiderQueen.addOnSpiderListener(this)
|
||||
suspend fun <T> useEhPageLoader(
|
||||
info: GalleryInfo,
|
||||
startPage: Int,
|
||||
block: suspend (PageLoader) -> T,
|
||||
) = resourceScope {
|
||||
val queen = install(
|
||||
{ obtainSpiderQueen(info, SpiderQueen.MODE_READ) },
|
||||
{ queen, _ -> releaseSpiderQueen(queen, SpiderQueen.MODE_READ) },
|
||||
)
|
||||
check(queen.awaitReady())
|
||||
val loader = autoCloseable {
|
||||
object : PageLoader(info.gid, startPage) {
|
||||
val listener = object : OnSpiderListener {
|
||||
override fun onGetPages(pages: Int) = Unit
|
||||
|
||||
override fun onGet509(index: Int) = Unit
|
||||
|
||||
override fun onPageDownload(index: Int, contentLength: Long, receivedSize: Long, bytesRead: Int) {
|
||||
if (contentLength > 0) {
|
||||
notifyPagePercent(index, receivedSize.toFloat() / contentLength)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int) = Unit
|
||||
|
||||
override fun onPageFailure(index: Int, error: String?, finished: Int, downloaded: Int, total: Int) = notifyPageFailed(index, error)
|
||||
|
||||
override fun onFinish(finished: Int, downloaded: Int, total: Int) = Unit
|
||||
|
||||
override fun onGetImageSuccess(index: Int, image: Image?) = notifyPageSucceed(index, image!!)
|
||||
|
||||
override fun onGetImageFailure(index: Int, error: String?) = notifyPageFailed(index, error)
|
||||
}.apply { queen.addOnSpiderListener(this) }
|
||||
|
||||
override fun close() {
|
||||
super.close()
|
||||
queen.removeOnSpiderListener(listener)
|
||||
}
|
||||
|
||||
override val title by lazy { EhUtils.getSuitableTitle(info) }
|
||||
|
||||
override fun getImageExtension(index: Int) = queen.getExtension(index)
|
||||
|
||||
override fun save(index: Int, file: Path) = queen.save(index, file)
|
||||
|
||||
override val size = queen.size
|
||||
|
||||
override fun prefetchPages(pages: List<Int>, bounds: Pair<Int, Int>) = queen.preloadPages(pages, bounds)
|
||||
|
||||
override fun onRequest(index: Int) = queen.request(index)
|
||||
|
||||
override fun onForceRequest(index: Int, orgImg: Boolean) = queen.forceRequest(index, orgImg)
|
||||
|
||||
override fun onCancelRequest(index: Int) = queen.cancelRequest(index)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
mSpiderQueen.removeOnSpiderListener(this)
|
||||
releaseSpiderQueen(mSpiderQueen, SpiderQueen.MODE_READ)
|
||||
}
|
||||
|
||||
override val title by lazy {
|
||||
EhUtils.getSuitableTitle(mGalleryInfo)
|
||||
}
|
||||
|
||||
override fun getImageExtension(index: Int): String? = mSpiderQueen.getExtension(index)
|
||||
|
||||
override fun save(index: Int, file: Path): Boolean = mSpiderQueen.save(index, file)
|
||||
|
||||
override val size: Int
|
||||
get() = mSpiderQueen.size
|
||||
|
||||
override fun onRequest(index: Int) {
|
||||
mSpiderQueen.request(index)
|
||||
}
|
||||
|
||||
override fun onForceRequest(index: Int, orgImg: Boolean) {
|
||||
mSpiderQueen.forceRequest(index, orgImg)
|
||||
}
|
||||
|
||||
override suspend fun awaitReady(): Boolean = super.awaitReady() && mSpiderQueen.awaitReady()
|
||||
|
||||
override val isReady: Boolean
|
||||
get() = ::mSpiderQueen.isInitialized && mSpiderQueen.isReady
|
||||
|
||||
override fun onCancelRequest(index: Int) {
|
||||
mSpiderQueen.cancelRequest(index)
|
||||
}
|
||||
|
||||
override fun onGetPages(pages: Int) {}
|
||||
|
||||
override fun onGet509(index: Int) {}
|
||||
|
||||
override fun onPageDownload(
|
||||
index: Int,
|
||||
contentLength: Long,
|
||||
receivedSize: Long,
|
||||
bytesRead: Int,
|
||||
) {
|
||||
if (contentLength > 0) {
|
||||
notifyPagePercent(index, receivedSize.toFloat() / contentLength)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageSuccess(index: Int, finished: Int, downloaded: Int, total: Int) {}
|
||||
override fun onPageFailure(
|
||||
index: Int,
|
||||
error: String?,
|
||||
finished: Int,
|
||||
downloaded: Int,
|
||||
total: Int,
|
||||
) {
|
||||
notifyPageFailed(index, error)
|
||||
}
|
||||
|
||||
override fun onFinish(finished: Int, downloaded: Int, total: Int) {}
|
||||
override fun onGetImageSuccess(index: Int, image: Image?) {
|
||||
notifyPageSucceed(index, image!!)
|
||||
}
|
||||
|
||||
override fun onGetImageFailure(index: Int, error: String?) {
|
||||
notifyPageFailed(index, error)
|
||||
}
|
||||
|
||||
override fun prefetchPages(pages: List<Int>, bounds: Pair<Int, Int>) {
|
||||
mSpiderQueen.preloadPages(pages, bounds)
|
||||
}
|
||||
loader.progressJob.join()
|
||||
block(loader)
|
||||
}
|
||||
@@ -1,21 +1,27 @@
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.collection.SieveCache
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.image.Image
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.OSUtils
|
||||
import com.hippo.ehviewer.util.isAtLeastO
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import kotlin.concurrent.read
|
||||
import kotlin.concurrent.write
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import okio.Path
|
||||
|
||||
private val progressScope = CoroutineScope(Dispatchers.IO)
|
||||
private const val MAX_CACHE_SIZE = 512 * 1024 * 1024
|
||||
private const val MIN_CACHE_SIZE = 128 * 1024 * 1024
|
||||
|
||||
abstract class PageLoader {
|
||||
abstract class PageLoader(val gid: Long, var startPage: Int) : AutoCloseable {
|
||||
private val cache = SieveCache<Int, Image>(
|
||||
maxSize = if (isAtLeastO) {
|
||||
(OSUtils.totalMemory / 16).toInt().coerceIn(MIN_CACHE_SIZE, MAX_CACHE_SIZE)
|
||||
@@ -42,19 +48,6 @@ abstract class PageLoader {
|
||||
|
||||
private val prefetchPageCount = Settings.preloadImage
|
||||
|
||||
@CallSuper
|
||||
open suspend fun awaitReady() = true
|
||||
|
||||
abstract val isReady: Boolean
|
||||
|
||||
@CallSuper
|
||||
abstract fun start()
|
||||
|
||||
@CallSuper
|
||||
open fun stop() {
|
||||
lock.write { cache.evictAll() }
|
||||
}
|
||||
|
||||
fun restart() {
|
||||
lock.write { cache.evictAll() }
|
||||
pages.forEach(Page::reset)
|
||||
@@ -138,4 +131,29 @@ abstract class PageLoader {
|
||||
fun notifyPageFailed(index: Int, error: String?) {
|
||||
pages[index].statusFlow.update { PageStatus.Error(error) }
|
||||
}
|
||||
|
||||
val progressJob = progressScope.launch {
|
||||
if (startPage == -1) {
|
||||
startPage = EhDB.getReadProgress(gid)
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
lock.write { cache.evictAll() }
|
||||
if (gid != 0L) {
|
||||
progressScope.launch {
|
||||
EhDB.putReadProgress(gid, startPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract val title: String
|
||||
|
||||
protected abstract fun getImageExtension(index: Int): String?
|
||||
|
||||
fun getImageFilename(index: Int): String? = getImageExtension(index)?.let {
|
||||
FileUtils.sanitizeFilename("$title - ${index + 1}.${it.lowercase()}")
|
||||
}
|
||||
|
||||
abstract fun save(index: Int, file: Path): Boolean
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* Copyright 2016 Hippo Seven
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.hippo.ehviewer.gallery
|
||||
|
||||
import com.hippo.ehviewer.EhDB
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import okio.Path
|
||||
|
||||
abstract class PageLoader2(private val gid: Long, var startPage: Int) :
|
||||
PageLoader(),
|
||||
CoroutineScope {
|
||||
override val coroutineContext = Dispatchers.IO + Job()
|
||||
private val progressJob = async(start = CoroutineStart.LAZY) {
|
||||
if (startPage == -1) {
|
||||
startPage = EhDB.getReadProgress(gid)
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
progressJob.start()
|
||||
}
|
||||
|
||||
override suspend fun awaitReady(): Boolean {
|
||||
progressJob.await()
|
||||
return super.awaitReady() && startPage != -1
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
super.stop()
|
||||
if (gid != 0L) {
|
||||
launch {
|
||||
EhDB.putReadProgress(gid, startPage)
|
||||
this@PageLoader2.cancel()
|
||||
}
|
||||
} else {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract val title: String
|
||||
|
||||
protected abstract fun getImageExtension(index: Int): String?
|
||||
|
||||
fun getImageFilename(index: Int): String? = getImageExtension(index)?.let {
|
||||
FileUtils.sanitizeFilename("$title - ${index + 1}.${it.lowercase()}")
|
||||
}
|
||||
|
||||
abstract fun save(index: Int, file: Path): Boolean
|
||||
}
|
||||
@@ -21,7 +21,7 @@ import androidx.compose.ui.util.fastFold
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.gallery.Page
|
||||
import com.hippo.ehviewer.gallery.PageLoader2
|
||||
import com.hippo.ehviewer.gallery.PageLoader
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType.CONTINUOUS_VERTICAL
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType.RIGHT_TO_LEFT
|
||||
@@ -37,7 +37,7 @@ fun GalleryPager(
|
||||
type: ReadingModeType,
|
||||
pagerState: PagerState,
|
||||
lazyListState: LazyListState,
|
||||
pageLoader: PageLoader2,
|
||||
pageLoader: PageLoader,
|
||||
showNavigationOverlay: Boolean,
|
||||
onNavigationModeChange: () -> Unit,
|
||||
onSelectPage: (Page) -> Unit,
|
||||
|
||||
@@ -17,7 +17,7 @@ import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.client.EhUrl
|
||||
import com.hippo.ehviewer.client.data.GalleryInfo
|
||||
import com.hippo.ehviewer.gallery.Page
|
||||
import com.hippo.ehviewer.gallery.PageLoader2
|
||||
import com.hippo.ehviewer.gallery.PageLoader
|
||||
import com.hippo.ehviewer.util.AppConfig
|
||||
import com.hippo.ehviewer.util.FileUtils
|
||||
import com.hippo.ehviewer.util.awaitActivityResult
|
||||
@@ -33,7 +33,7 @@ import moe.tarsin.coroutines.runSuspendCatching
|
||||
import okio.Path.Companion.toOkioPath
|
||||
import splitties.systemservices.clipboardManager
|
||||
|
||||
context(PageLoader2)
|
||||
context(PageLoader)
|
||||
private fun Context.provideImage(index: Int): Uri? {
|
||||
val dir = AppConfig.externalTempDir ?: return null
|
||||
val name = getImageFilename(index) ?: return null
|
||||
@@ -41,7 +41,7 @@ private fun Context.provideImage(index: Int): Uri? {
|
||||
return FileProvider.getUriForFile(this, "$APPLICATION_ID.fileprovider", file)
|
||||
}
|
||||
|
||||
context(SnackbarHostState, Context, PageLoader2)
|
||||
context(SnackbarHostState, Context, PageLoader)
|
||||
suspend fun shareImage(page: Page, info: GalleryInfo? = null) {
|
||||
val error = getString(R.string.error_cant_save_image)
|
||||
val share = getString(R.string.share_image)
|
||||
@@ -66,7 +66,7 @@ suspend fun shareImage(page: Page, info: GalleryInfo? = null) {
|
||||
}
|
||||
}
|
||||
|
||||
context(SnackbarHostState, Context, PageLoader2)
|
||||
context(SnackbarHostState, Context, PageLoader)
|
||||
suspend fun copy(page: Page) {
|
||||
val error = getString(R.string.error_cant_save_image)
|
||||
val copied = getString(R.string.copied_to_clipboard)
|
||||
@@ -82,7 +82,7 @@ suspend fun copy(page: Page) {
|
||||
}
|
||||
}
|
||||
|
||||
context(SnackbarHostState, Context, PageLoader2)
|
||||
context(SnackbarHostState, Context, PageLoader)
|
||||
suspend fun save(page: Page) {
|
||||
val granted = isAtLeastQ || requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
val cannotSave = getString(R.string.error_cant_save_image)
|
||||
@@ -136,7 +136,7 @@ suspend fun save(page: Page) {
|
||||
}
|
||||
}
|
||||
|
||||
context(SnackbarHostState, Context, PageLoader2)
|
||||
context(SnackbarHostState, Context, PageLoader)
|
||||
suspend fun saveTo(page: Page) {
|
||||
val filename = getImageFilename(page.index)
|
||||
if (filename == null) {
|
||||
|
||||
@@ -31,7 +31,7 @@ import arrow.core.partially1
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.gallery.Page
|
||||
import com.hippo.ehviewer.gallery.PageLoader2
|
||||
import com.hippo.ehviewer.gallery.PageLoader
|
||||
import com.hippo.ehviewer.gallery.PageStatus
|
||||
import com.hippo.ehviewer.gallery.statusObserved
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.NavigationRegions
|
||||
@@ -53,7 +53,7 @@ fun PagerViewer(
|
||||
pagerState: PagerState,
|
||||
isRtl: Boolean,
|
||||
isVertical: Boolean,
|
||||
pageLoader: PageLoader2,
|
||||
pageLoader: PageLoader,
|
||||
navigator: () -> NavigationRegions,
|
||||
onSelectPage: (Page) -> Unit,
|
||||
onMenuRegionClick: () -> Unit,
|
||||
@@ -122,7 +122,7 @@ fun PagerViewer(
|
||||
@Composable
|
||||
private fun PageContainer(
|
||||
page: Page,
|
||||
pageLoader: PageLoader2,
|
||||
pageLoader: PageLoader,
|
||||
isRtl: Boolean,
|
||||
scaleType: Int,
|
||||
landscapeZoom: Boolean,
|
||||
|
||||
@@ -49,10 +49,11 @@ import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import arrow.core.Either
|
||||
import arrow.core.Either.Companion.catch
|
||||
import arrow.core.raise.ensure
|
||||
import arrow.core.right
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import com.hippo.ehviewer.R
|
||||
import com.hippo.ehviewer.Settings
|
||||
@@ -60,20 +61,19 @@ import com.hippo.ehviewer.client.data.BaseGalleryInfo
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.download.DownloadManager
|
||||
import com.hippo.ehviewer.download.archiveFile
|
||||
import com.hippo.ehviewer.gallery.ArchivePageLoader
|
||||
import com.hippo.ehviewer.gallery.EhPageLoader
|
||||
import com.hippo.ehviewer.gallery.Page
|
||||
import com.hippo.ehviewer.gallery.PageLoader2
|
||||
import com.hippo.ehviewer.gallery.PageLoader
|
||||
import com.hippo.ehviewer.gallery.PageStatus
|
||||
import com.hippo.ehviewer.gallery.status
|
||||
import com.hippo.ehviewer.gallery.unblock
|
||||
import com.hippo.ehviewer.gallery.useArchivePageLoader
|
||||
import com.hippo.ehviewer.gallery.useEhPageLoader
|
||||
import com.hippo.ehviewer.ui.composing
|
||||
import com.hippo.ehviewer.ui.theme.EhTheme
|
||||
import com.hippo.ehviewer.ui.tools.Await
|
||||
import com.hippo.ehviewer.ui.tools.DialogState
|
||||
import com.hippo.ehviewer.ui.tools.EmptyWindowInsets
|
||||
import com.hippo.ehviewer.ui.tools.asyncInVM
|
||||
import com.hippo.ehviewer.ui.tools.launchInVM
|
||||
import com.hippo.ehviewer.util.displayString
|
||||
import com.hippo.ehviewer.util.hasAds
|
||||
import com.hippo.files.toOkioPath
|
||||
@@ -86,13 +86,13 @@ import eu.kanade.tachiyomi.ui.reader.ReaderContentOverlay
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderPageSheetMeta
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.withIOContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import moe.tarsin.kt.unreachable
|
||||
|
||||
@@ -115,58 +115,62 @@ private fun Background(
|
||||
@Destination<RootGraph>
|
||||
@Composable
|
||||
fun AnimatedVisibilityScope.ReaderScreen(args: ReaderScreenArgs, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
val pageLoader by asyncInVM(args) { preparePageLoader(args) }
|
||||
Await({ pageLoader.await() }) { loader ->
|
||||
launchInVM(loader) {
|
||||
loader.start()
|
||||
try {
|
||||
awaitCancellation()
|
||||
} finally {
|
||||
loader.stop()
|
||||
}
|
||||
val bgColor by collectBackgroundColorAsState()
|
||||
val uiController = rememberSystemUiController()
|
||||
DisposableEffect(uiController) {
|
||||
val lightStatusBar = uiController.statusBarDarkContentEnabled
|
||||
onDispose {
|
||||
uiController.statusBarDarkContentEnabled = lightStatusBar
|
||||
}
|
||||
val bgColor by collectBackgroundColorAsState()
|
||||
val readingFailed = stringResource(R.string.error_reading_failed)
|
||||
val uiController = rememberSystemUiController()
|
||||
DisposableEffect(uiController) {
|
||||
val lightStatusBar = uiController.statusBarDarkContentEnabled
|
||||
onDispose {
|
||||
uiController.statusBarDarkContentEnabled = lightStatusBar
|
||||
}
|
||||
}
|
||||
LaunchedEffect(uiController) {
|
||||
snapshotFlow { bgColor }.collect {
|
||||
uiController.statusBarDarkContentEnabled = it == Color.White
|
||||
}
|
||||
LaunchedEffect(uiController) {
|
||||
snapshotFlow { bgColor }.collect {
|
||||
uiController.statusBarDarkContentEnabled = it == Color.White
|
||||
}
|
||||
}
|
||||
Await(
|
||||
block = { catch { readingFailed.takeUnless { loader.awaitReady() } }.fold({ it.displayString() }, { it }) },
|
||||
placeholder = {
|
||||
Background(bgColor) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
|
||||
Await(
|
||||
block = asyncInVM(args) { alive ->
|
||||
suspendCancellableCoroutine { cont ->
|
||||
alive.launchIO {
|
||||
catch {
|
||||
usePageLoader(args) { loader ->
|
||||
cont.resume(loader.right())
|
||||
awaitCancellation()
|
||||
}
|
||||
}.let { left -> cont.resume(left) }
|
||||
}
|
||||
},
|
||||
) { error ->
|
||||
if (error == null) {
|
||||
}
|
||||
}.value.run {
|
||||
{ await() }
|
||||
},
|
||||
placeholder = {
|
||||
Background(bgColor) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
},
|
||||
) { result ->
|
||||
when (result) {
|
||||
is Either.Left -> Background(bgColor) {
|
||||
Text(
|
||||
text = result.value.displayString(),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
}
|
||||
is Either.Right -> {
|
||||
val loader = result.value
|
||||
val info = (args as? ReaderScreenArgs.Gallery)?.info
|
||||
key(loader) {
|
||||
ReaderScreen(loader, info, navigator)
|
||||
}
|
||||
} else {
|
||||
Background(bgColor) {
|
||||
Text(
|
||||
text = error,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AnimatedVisibilityScope.ReaderScreen(pageLoader: PageLoader2, info: BaseGalleryInfo?, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
fun AnimatedVisibilityScope.ReaderScreen(pageLoader: PageLoader, info: BaseGalleryInfo?, navigator: DestinationsNavigator) = composing(navigator) {
|
||||
ConfigureKeepScreenOn()
|
||||
LaunchedEffect(Unit) {
|
||||
val orientation = requestedOrientation
|
||||
@@ -364,19 +368,20 @@ fun AnimatedVisibilityScope.ReaderScreen(pageLoader: PageLoader2, info: BaseGall
|
||||
}
|
||||
|
||||
context(Context, DialogState, DestinationsNavigator)
|
||||
private suspend fun preparePageLoader(args: ReaderScreenArgs) = when (args) {
|
||||
is ReaderScreenArgs.Gallery -> withIOContext {
|
||||
suspend fun <T> usePageLoader(args: ReaderScreenArgs, block: suspend (PageLoader) -> T) = when (args) {
|
||||
is ReaderScreenArgs.Gallery -> {
|
||||
val info = args.info
|
||||
val page = args.page
|
||||
val archive = DownloadManager.getDownloadInfo(info.gid)?.archiveFile
|
||||
if (archive != null) {
|
||||
ArchivePageLoader(archive, info.gid, page, info.hasAds)
|
||||
useArchivePageLoader(archive, info.gid, page, info.hasAds, block = block)
|
||||
} else {
|
||||
EhPageLoader(info, page)
|
||||
useEhPageLoader(info, page, block)
|
||||
}
|
||||
}
|
||||
is ReaderScreenArgs.Archive -> {
|
||||
ArchivePageLoader(args.uri.toOkioPath()) { invalidator ->
|
||||
is ReaderScreenArgs.Archive -> useArchivePageLoader(
|
||||
args.uri.toOkioPath(),
|
||||
passwdProvider = { invalidator ->
|
||||
awaitInputText(
|
||||
title = getString(R.string.archive_need_passwd),
|
||||
hint = getString(R.string.archive_passwd),
|
||||
@@ -385,8 +390,9 @@ private suspend fun preparePageLoader(args: ReaderScreenArgs) = when (args) {
|
||||
ensure(text.isNotBlank()) { getString(R.string.passwd_cannot_be_empty) }
|
||||
ensure(invalidator(text)) { getString(R.string.passwd_wrong) }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
block = block,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -11,7 +11,7 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import com.hippo.ehviewer.gallery.PageLoader2
|
||||
import com.hippo.ehviewer.gallery.PageLoader
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
|
||||
@@ -19,7 +19,7 @@ import kotlinx.coroutines.flow.filterNotNull
|
||||
class SliderPagerDoubleSync(
|
||||
private val lazyListState: LazyListState,
|
||||
private val pagerState: PagerState,
|
||||
private val pageLoader: PageLoader2,
|
||||
private val pageLoader: PageLoader,
|
||||
) {
|
||||
private var sliderFollowPager by mutableStateOf(true)
|
||||
var sliderValue by mutableIntStateOf(pageLoader.startPage + 1)
|
||||
@@ -80,7 +80,7 @@ class SliderPagerDoubleSync(
|
||||
fun rememberSliderPagerDoubleSyncState(
|
||||
lazyListState: LazyListState,
|
||||
pagerState: PagerState,
|
||||
pageLoader: PageLoader2,
|
||||
pageLoader: PageLoader,
|
||||
): SliderPagerDoubleSync = remember {
|
||||
SliderPagerDoubleSync(lazyListState, pagerState, pageLoader)
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp
|
||||
import com.hippo.ehviewer.Settings
|
||||
import com.hippo.ehviewer.collectAsState
|
||||
import com.hippo.ehviewer.gallery.Page
|
||||
import com.hippo.ehviewer.gallery.PageLoader2
|
||||
import com.hippo.ehviewer.gallery.PageLoader
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.NavigationRegions
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.getAction
|
||||
@@ -32,7 +32,7 @@ import me.saket.telephoto.zoomable.zoomable
|
||||
fun WebtoonViewer(
|
||||
lazyListState: LazyListState,
|
||||
withGaps: Boolean,
|
||||
pageLoader: PageLoader2,
|
||||
pageLoader: PageLoader,
|
||||
navigator: () -> NavigationRegions,
|
||||
onSelectPage: (Page) -> Unit,
|
||||
onMenuRegionClick: () -> Unit,
|
||||
|
||||
@@ -20,6 +20,7 @@ import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
@@ -111,14 +112,18 @@ fun <R> asyncInVM(
|
||||
key: Any?,
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> R,
|
||||
block: suspend CoroutineScope.(CoroutineScope) -> R,
|
||||
) = rememberUpdatedStateInVM(key).let { key ->
|
||||
val f by rememberUpdatedStateInVM(block)
|
||||
rememberInVM {
|
||||
nonNullState {
|
||||
viewModelScope.launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
snapshotFlow { key.value }.mapLatest {
|
||||
coroutineScope { value = async(context, start, f) }
|
||||
coroutineScope {
|
||||
val outerScope = this
|
||||
value = async(context, start) { f(outerScope) }
|
||||
awaitCancellation()
|
||||
}
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user