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:
renovate[bot]andDude so hot authored and GitHub committed 2024-10-29 08:10:52 +00:00
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()
}
}