diff --git a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt index b98fe1a3a..1d826596c 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/Settings.kt @@ -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) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt b/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt index 5b97b5b85..b83479138 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt @@ -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 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() + private val mutex = NamedMutex() + 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() - private val workerMutex = NamedMutex() - 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, bounds: Pair) = 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, bounds: Pair) = Unit } - -private const val DEBUG_TAG = "ArchivePageLoader" diff --git a/app/src/main/kotlin/com/hippo/ehviewer/gallery/EhPageLoader.kt b/app/src/main/kotlin/com/hippo/ehviewer/gallery/EhPageLoader.kt index 9f16f2e06..4241ebe82 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/gallery/EhPageLoader.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/gallery/EhPageLoader.kt @@ -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 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, bounds: Pair) = 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, bounds: Pair) { - mSpiderQueen.preloadPages(pages, bounds) - } + loader.progressJob.join() + block(loader) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader.kt b/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader.kt index 087363b21..abecbf927 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader.kt @@ -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( 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 } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader2.kt b/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader2.kt deleted file mode 100644 index 22466d533..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader2.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/GalleryPager.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/GalleryPager.kt index 283dafbac..6ea5f9c90 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/GalleryPager.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/GalleryPager.kt @@ -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, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt index fe18a69d6..da5b19afa 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PageActions.kt @@ -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) { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PagerViewer.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PagerViewer.kt index e502e8ec0..3605da540 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PagerViewer.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/PagerViewer.kt @@ -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, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt index 1c6192354..1e994a69e 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/ReaderScreen.kt @@ -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 @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 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 diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/SliderPagerDoubleSync.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/SliderPagerDoubleSync.kt index d00414735..0285c1851 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/SliderPagerDoubleSync.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/SliderPagerDoubleSync.kt @@ -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) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/WebtoonViewer.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/WebtoonViewer.kt index cdac8d9f3..e9ac9bced 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/WebtoonViewer.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/reader/WebtoonViewer.kt @@ -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, diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/ViewModelState.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/ViewModelState.kt index b3c26d53e..a94d6b343 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/ViewModelState.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/tools/ViewModelState.kt @@ -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 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() } }