Преглед на файлове

feat: 依旧漫画优化

HapeLee преди 2 месеца
родител
ревизия
46f25b8731

+ 1 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -123,6 +123,7 @@ object PreferKey {
     const val preDownloadNum = "preDownloadNum"
     const val preDownloadNum = "preDownloadNum"
     const val mangaPreDownloadNum = "mangaPreDownloadNum"
     const val mangaPreDownloadNum = "mangaPreDownloadNum"
     const val mangaChapterPrefetchCount = "mangaChapterPrefetchCount"
     const val mangaChapterPrefetchCount = "mangaChapterPrefetchCount"
+    const val mangaAutoOfflineCache = "mangaAutoOfflineCache"
     const val mangaAutoPageSpeed = "mangaAutoPageSpeed"
     const val mangaAutoPageSpeed = "mangaAutoPageSpeed"
     const val mangaFooterConfig = "mangaFooterConfig"
     const val mangaFooterConfig = "mangaFooterConfig"
     const val disableClickScroll = "disableClickScroll"
     const val disableClickScroll = "disableClickScroll"

+ 4 - 1
app/src/main/java/io/legado/app/data/repository/MangaSettingsRepository.kt

@@ -38,6 +38,7 @@ internal fun MangaSettings.toPrefMap(): Map<String, Any?> = mapOf(
     PreferKey.mangaScrollMode to scrollMode,
     PreferKey.mangaScrollMode to scrollMode,
     PreferKey.mangaPreDownloadNum to preDownloadNum,
     PreferKey.mangaPreDownloadNum to preDownloadNum,
     PreferKey.mangaChapterPrefetchCount to chapterPrefetchCount,
     PreferKey.mangaChapterPrefetchCount to chapterPrefetchCount,
+    PreferKey.mangaAutoOfflineCache to autoOfflineCache,
     PreferKey.mangaAutoPageSpeed to autoPageSpeed,
     PreferKey.mangaAutoPageSpeed to autoPageSpeed,
     PreferKey.mangaFooterConfig to footerConfig,
     PreferKey.mangaFooterConfig to footerConfig,
     PreferKey.disableClickScroll to disableClickScroll,
     PreferKey.disableClickScroll to disableClickScroll,
@@ -85,7 +86,9 @@ internal fun Preferences.toMangaSettings(): MangaSettings = MangaSettings(
     disableMangaCrossFade = compatDsBoolean(PreferKey.disableMangaCrossFade) ?: false,
     disableMangaCrossFade = compatDsBoolean(PreferKey.disableMangaCrossFade) ?: false,
     scrollMode = compatDsInt(PreferKey.mangaScrollMode) ?: 4,
     scrollMode = compatDsInt(PreferKey.mangaScrollMode) ?: 4,
     preDownloadNum = compatDsInt(PreferKey.mangaPreDownloadNum) ?: 10,
     preDownloadNum = compatDsInt(PreferKey.mangaPreDownloadNum) ?: 10,
-    chapterPrefetchCount = compatDsInt(PreferKey.mangaChapterPrefetchCount) ?: 1,
+    // Chapter prefetch queues the offline cache downloader, so it must be opt-in.
+    chapterPrefetchCount = compatDsInt(PreferKey.mangaChapterPrefetchCount) ?: 0,
+    autoOfflineCache = compatDsBoolean(PreferKey.mangaAutoOfflineCache) ?: false,
     autoPageSpeed = compatDsInt(PreferKey.mangaAutoPageSpeed) ?: 3,
     autoPageSpeed = compatDsInt(PreferKey.mangaAutoPageSpeed) ?: 3,
     footerConfig = compatDsString(PreferKey.mangaFooterConfig).orEmpty(),
     footerConfig = compatDsString(PreferKey.mangaFooterConfig).orEmpty(),
     disableClickScroll = compatDsBoolean(PreferKey.disableClickScroll) ?: false,
     disableClickScroll = compatDsBoolean(PreferKey.disableClickScroll) ?: false,

+ 2 - 1
app/src/main/java/io/legado/app/domain/model/settings/MangaSettings.kt

@@ -7,7 +7,8 @@ data class MangaSettings(
     val disableMangaCrossFade: Boolean = false,
     val disableMangaCrossFade: Boolean = false,
     val scrollMode: Int = 4,
     val scrollMode: Int = 4,
     val preDownloadNum: Int = 10,
     val preDownloadNum: Int = 10,
-    val chapterPrefetchCount: Int = 1,
+    val chapterPrefetchCount: Int = 0,
+    val autoOfflineCache: Boolean = false,
     val autoPageSpeed: Int = 3,
     val autoPageSpeed: Int = 3,
     val footerConfig: String = "",
     val footerConfig: String = "",
     val disableClickScroll: Boolean = false,
     val disableClickScroll: Boolean = false,

+ 2 - 0
app/src/main/java/io/legado/app/help/storage/BackupConfig.kt

@@ -348,6 +348,8 @@ object BackupConfig {
         PreferKey.mangaScrollMode,
         PreferKey.mangaScrollMode,
         PreferKey.webtoonSidePaddingDp,
         PreferKey.webtoonSidePaddingDp,
         PreferKey.mangaPreDownloadNum,
         PreferKey.mangaPreDownloadNum,
+        PreferKey.mangaChapterPrefetchCount,
+        PreferKey.mangaAutoOfflineCache,
         PreferKey.mangaAutoPageSpeed,
         PreferKey.mangaAutoPageSpeed,
         PreferKey.mangaFooterConfig,
         PreferKey.mangaFooterConfig,
         PreferKey.disableClickScroll,
         PreferKey.disableClickScroll,

+ 9 - 2
app/src/main/java/io/legado/app/model/CacheBook.kt

@@ -9,6 +9,7 @@ import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSource
 import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isLocal
+import io.legado.app.model.CacheBook.explicitFifo
 import io.legado.app.model.cache.CacheDownloadRequest
 import io.legado.app.model.cache.CacheDownloadRequest
 import io.legado.app.model.cache.CacheDownloadStateStore
 import io.legado.app.model.cache.CacheDownloadStateStore
 import io.legado.app.model.cache.ChapterSelection
 import io.legado.app.model.cache.ChapterSelection
@@ -301,7 +302,11 @@ object CacheBook {
     fun start(context: Context, request: CacheDownloadRequest, isLocal: Boolean = false) {
     fun start(context: Context, request: CacheDownloadRequest, isLocal: Boolean = false) {
         if (isLocal) return
         if (isLocal) return
         if (!request.hasValidSelection()) return
         if (!request.hasValidSelection()) return
-        isPaused = false
+        // Reading preloads must obey a user-paused download queue. Manual and batch requests
+        // remain explicit resume actions.
+        if (request.source != io.legado.app.model.cache.CacheDownloadSource.ReadPreload) {
+            isPaused = false
+        }
         startCacheBookService(context) {
         startCacheBookService(context) {
             action = IntentAction.start
             action = IntentAction.start
             putRequestExtras(request)
             putRequestExtras(request)
@@ -321,7 +326,9 @@ object CacheBook {
         val finalRequests = validRequests.filterNot { it.bookUrl in localBookUrls }
         val finalRequests = validRequests.filterNot { it.bookUrl in localBookUrls }
         if (finalRequests.isEmpty()) return@withContext
         if (finalRequests.isEmpty()) return@withContext
 
 
-        isPaused = false
+        if (validRequests.any { it.source != io.legado.app.model.cache.CacheDownloadSource.ReadPreload }) {
+            isPaused = false
+        }
         // 如果请求较多,可以通过 Intent 传递一个特殊的标志让 Service 自己去检查队列,
         // 如果请求较多,可以通过 Intent 传递一个特殊的标志让 Service 自己去检查队列,
         // 或者分批发送。这里我们先简单处理,但确保不在主线程做数据库查询。
         // 或者分批发送。这里我们先简单处理,但确保不在主线程做数据库查询。
         finalRequests.forEach { request ->
         finalRequests.forEach { request ->

+ 6 - 6
app/src/main/java/io/legado/app/model/CacheBookModel.kt

@@ -4,18 +4,17 @@ import io.legado.app.constant.AppLog
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSource
-import io.legado.app.help.book.isImage
 import io.legado.app.exception.ConcurrentException
 import io.legado.app.exception.ConcurrentException
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.coroutine.CompositeCoroutine
 import io.legado.app.help.coroutine.CompositeCoroutine
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.coroutine.Coroutine
+import io.legado.app.model.cache.CacheChapterProgress
+import io.legado.app.model.cache.CacheChapterProgressPhase
 import io.legado.app.model.cache.CacheDownloadCandidate
 import io.legado.app.model.cache.CacheDownloadCandidate
 import io.legado.app.model.cache.CacheDownloadQueue
 import io.legado.app.model.cache.CacheDownloadQueue
 import io.legado.app.model.cache.CacheDownloadRepository
 import io.legado.app.model.cache.CacheDownloadRepository
 import io.legado.app.model.cache.CacheDownloadRequest
 import io.legado.app.model.cache.CacheDownloadRequest
 import io.legado.app.model.cache.CacheDownloadSource
 import io.legado.app.model.cache.CacheDownloadSource
-import io.legado.app.model.cache.CacheChapterProgress
-import io.legado.app.model.cache.CacheChapterProgressPhase
 import io.legado.app.model.cache.CacheDownloadStateStore
 import io.legado.app.model.cache.CacheDownloadStateStore
 import io.legado.app.model.cache.ChapterSelection
 import io.legado.app.model.cache.ChapterSelection
 import kotlinx.coroutines.CancellationException
 import kotlinx.coroutines.CancellationException
@@ -297,11 +296,12 @@ class CacheBookModel(
         synchronized(this) {
         synchronized(this) {
             isStopped = false
             isStopped = false
             val selected = selectionIndices(request.selection)
             val selected = selectionIndices(request.selection)
-            // 整书暂停时再入队:只解冻本次请求的章节,其余等待章转为单章暂停
-            if (isPaused) {
+            // An explicit request resumes only its selected chapters. Reader preloads must not
+            // override a user's pause for this book or the global download queue.
+            if (isPaused && request.source != CacheDownloadSource.ReadPreload) {
                 releaseBookPauseKeepingOnly(selected)
                 releaseBookPauseKeepingOnly(selected)
+                isPaused = false
             }
             }
-            isPaused = false
             when (val selection = request.selection) {
             when (val selection = request.selection) {
                 is ChapterSelection.Range -> {
                 is ChapterSelection.Range -> {
                     canceledDownloadSet.removeAll { it in selection.start..selection.end }
                     canceledDownloadSet.removeAll { it in selection.start..selection.end }

+ 5 - 1
app/src/main/java/io/legado/app/ui/book/manga/MangaReaderContract.kt

@@ -122,7 +122,8 @@ data class MangaReaderSettings(
     val disableClickScroll: Boolean = false,
     val disableClickScroll: Boolean = false,
     val longPressEnabled: Boolean = true,
     val longPressEnabled: Boolean = true,
     val preDownloadCount: Int = 10,
     val preDownloadCount: Int = 10,
-    val chapterPrefetchCount: Int = 1,
+    val chapterPrefetchCount: Int = 0,
+    val autoOfflineCache: Boolean = false,
     val autoReadSpeed: Int = 3,
     val autoReadSpeed: Int = 3,
     val volumeKeyPage: Boolean = false,
     val volumeKeyPage: Boolean = false,
     val reverseVolumeKeyPage: Boolean = false,
     val reverseVolumeKeyPage: Boolean = false,
@@ -241,6 +242,8 @@ sealed interface MangaReaderIntent {
         val itemIndex: Int,
         val itemIndex: Int,
         val firstItemIndex: Int = itemIndex,
         val firstItemIndex: Int = itemIndex,
         val lastItemIndex: Int = itemIndex,
         val lastItemIndex: Int = itemIndex,
+        /** Adjacent chapters may be precomposed; only promote after the current chapter leaves view. */
+        val currentChapterVisible: Boolean,
         val navigationId: Long,
         val navigationId: Long,
     ) : MangaReaderIntent
     ) : MangaReaderIntent
     data class PagerScrollChanged(val inProgress: Boolean) : MangaReaderIntent
     data class PagerScrollChanged(val inProgress: Boolean) : MangaReaderIntent
@@ -299,6 +302,7 @@ enum class MangaReaderSettingKey {
     LONG_PRESS,
     LONG_PRESS,
     PRE_DOWNLOAD,
     PRE_DOWNLOAD,
     CHAPTER_PREFETCH,
     CHAPTER_PREFETCH,
+    AUTO_OFFLINE_CACHE,
     AUTO_READ_SPEED,
     AUTO_READ_SPEED,
     VOLUME_KEY_PAGE,
     VOLUME_KEY_PAGE,
     REVERSE_VOLUME_KEY_PAGE,
     REVERSE_VOLUME_KEY_PAGE,

+ 7 - 2
app/src/main/java/io/legado/app/ui/book/manga/MangaReaderInteraction.kt

@@ -73,9 +73,14 @@ enum class MangaChapterSwitch { NONE, NEXT, PREVIOUS }
 internal fun mangaChapterSwitchDecision(
 internal fun mangaChapterSwitchDecision(
     currentChapterIndex: Int,
     currentChapterIndex: Int,
     visibleChapterIndex: Int,
     visibleChapterIndex: Int,
+    currentChapterVisible: Boolean,
 ): MangaChapterSwitch = when {
 ): MangaChapterSwitch = when {
-    currentChapterIndex < visibleChapterIndex -> MangaChapterSwitch.NEXT
-    currentChapterIndex > visibleChapterIndex -> MangaChapterSwitch.PREVIOUS
+    currentChapterIndex < visibleChapterIndex ->
+        if (currentChapterVisible) MangaChapterSwitch.NONE else MangaChapterSwitch.NEXT
+
+    currentChapterIndex > visibleChapterIndex ->
+        if (currentChapterVisible) MangaChapterSwitch.NONE else MangaChapterSwitch.PREVIOUS
+
     else -> MangaChapterSwitch.NONE
     else -> MangaChapterSwitch.NONE
 }
 }
 
 

+ 12 - 2
app/src/main/java/io/legado/app/ui/book/manga/MangaReaderScreen.kt

@@ -472,19 +472,24 @@ private fun WebtoonMangaList(
     LaunchedEffect(listState, state.pages, state.navigationId) {
     LaunchedEffect(listState, state.pages, state.navigationId) {
         snapshotFlow {
         snapshotFlow {
             val visibleItems = listState.layoutInfo.visibleItemsInfo
             val visibleItems = listState.layoutInfo.visibleItemsInfo
+            val currentChapterVisible = visibleItems.any { visibleItem ->
+                (state.pages.getOrNull(visibleItem.index) as? MangaReaderItemUi.Page)
+                    ?.chapterIndex == state.chapterIndex
+            }
             val firstItemIndex = visibleItems.firstOrNull()?.index
             val firstItemIndex = visibleItems.firstOrNull()?.index
             val focusedItemIndex = visibleItems.lastOrNull()?.index
             val focusedItemIndex = visibleItems.lastOrNull()?.index
             if (firstItemIndex == null || focusedItemIndex == null) null
             if (firstItemIndex == null || focusedItemIndex == null) null
-            else focusedItemIndex to firstItemIndex
+            else Triple(focusedItemIndex, firstItemIndex, currentChapterVisible)
         }
         }
             .distinctUntilChanged()
             .distinctUntilChanged()
             .collect { entry ->
             .collect { entry ->
-                entry?.let { (focusedIndex, firstIndex) ->
+                entry?.let { (focusedIndex, firstIndex, currentChapterVisible) ->
                     when (val item = state.pages.getOrNull(focusedIndex)) {
                     when (val item = state.pages.getOrNull(focusedIndex)) {
                         is MangaReaderItemUi.Page -> onIntent(MangaReaderIntent.VisibleItemChanged(
                         is MangaReaderItemUi.Page -> onIntent(MangaReaderIntent.VisibleItemChanged(
                             itemIndex = focusedIndex,
                             itemIndex = focusedIndex,
                             firstItemIndex = firstIndex,
                             firstItemIndex = firstIndex,
                             lastItemIndex = focusedIndex,
                             lastItemIndex = focusedIndex,
+                            currentChapterVisible = currentChapterVisible,
                             navigationId = state.navigationId,
                             navigationId = state.navigationId,
                         ))
                         ))
                         is MangaReaderItemUi.ChapterTransition -> Unit
                         is MangaReaderItemUi.ChapterTransition -> Unit
@@ -707,6 +712,10 @@ private fun HorizontalMangaPager(
                             itemIndex = index,
                             itemIndex = index,
                             firstItemIndex = indices.first(),
                             firstItemIndex = indices.first(),
                             lastItemIndex = indices.last(),
                             lastItemIndex = indices.last(),
+                            currentChapterVisible = indices.any { visibleIndex ->
+                                (state.pages.getOrNull(visibleIndex) as? MangaReaderItemUi.Page)
+                                    ?.chapterIndex == state.chapterIndex
+                            },
                             navigationId = state.navigationId,
                             navigationId = state.navigationId,
                         ))
                         ))
                         is MangaReaderItemUi.ChapterTransition -> Unit
                         is MangaReaderItemUi.ChapterTransition -> Unit
@@ -961,6 +970,7 @@ private fun VerticalMangaPager(
                 when (item) {
                 when (item) {
                     is MangaReaderItemUi.Page -> onIntent(MangaReaderIntent.VisibleItemChanged(
                     is MangaReaderItemUi.Page -> onIntent(MangaReaderIntent.VisibleItemChanged(
                         itemIndex = page,
                         itemIndex = page,
+                        currentChapterVisible = item.chapterIndex == state.chapterIndex,
                         navigationId = state.navigationId,
                         navigationId = state.navigationId,
                     ))
                     ))
                     is MangaReaderItemUi.ChapterTransition -> Unit
                     is MangaReaderItemUi.ChapterTransition -> Unit

+ 16 - 4
app/src/main/java/io/legado/app/ui/book/manga/MangaReaderViewModel.kt

@@ -83,7 +83,11 @@ class MangaReaderViewModel(
         viewModelScope.launch {
         viewModelScope.launch {
             mangaSettingsGateway.settings.collect { settings ->
             mangaSettingsGateway.settings.collect { settings ->
                 latestMangaSettings = settings
                 latestMangaSettings = settings
-                readerSession.execute(MangaSessionCommand.PrefetchCountChanged(settings.chapterPrefetchCount))
+                readerSession.execute(
+                    MangaSessionCommand.PrefetchCountChanged(
+                        if (settings.autoOfflineCache) settings.chapterPrefetchCount else 0,
+                    )
+                )
                 _uiState.update { state ->
                 _uiState.update { state ->
                     state.copy(settings = readSettings(settings))
                     state.copy(settings = readSettings(settings))
                 }
                 }
@@ -262,6 +266,7 @@ class MangaReaderViewModel(
                 intent.itemIndex,
                 intent.itemIndex,
                 intent.firstItemIndex,
                 intent.firstItemIndex,
                 intent.lastItemIndex,
                 intent.lastItemIndex,
+                intent.currentChapterVisible,
                 intent.navigationId,
                 intent.navigationId,
             )
             )
             is MangaReaderIntent.PagerScrollChanged -> {
             is MangaReaderIntent.PagerScrollChanged -> {
@@ -599,6 +604,7 @@ class MangaReaderViewModel(
                 itemIndex = targetIndex,
                 itemIndex = targetIndex,
                 firstItemIndex = targetIndex,
                 firstItemIndex = targetIndex,
                 lastItemIndex = targetIndex,
                 lastItemIndex = targetIndex,
+                currentChapterVisible = true,
                 navigationId = _uiState.value.navigationId,
                 navigationId = _uiState.value.navigationId,
             )
             )
         }
         }
@@ -781,6 +787,9 @@ class MangaReaderViewModel(
             MangaReaderSettingKey.CHAPTER_PREFETCH -> updateMangaPreference {
             MangaReaderSettingKey.CHAPTER_PREFETCH -> updateMangaPreference {
                 it.copy(chapterPrefetchCount = value)
                 it.copy(chapterPrefetchCount = value)
             }
             }
+            MangaReaderSettingKey.AUTO_OFFLINE_CACHE -> updateMangaPreference {
+                it.copy(autoOfflineCache = enabled)
+            }
             MangaReaderSettingKey.AUTO_READ_SPEED -> updateMangaPreference { it.copy(autoPageSpeed = value) }
             MangaReaderSettingKey.AUTO_READ_SPEED -> updateMangaPreference { it.copy(autoPageSpeed = value) }
             MangaReaderSettingKey.VOLUME_KEY_PAGE -> updateMangaPreference { it.copy(volumeKeyPage = enabled) }
             MangaReaderSettingKey.VOLUME_KEY_PAGE -> updateMangaPreference { it.copy(volumeKeyPage = enabled) }
             MangaReaderSettingKey.REVERSE_VOLUME_KEY_PAGE -> updateMangaPreference {
             MangaReaderSettingKey.REVERSE_VOLUME_KEY_PAGE -> updateMangaPreference {
@@ -1090,6 +1099,7 @@ class MangaReaderViewModel(
         itemIndex: Int,
         itemIndex: Int,
         firstItemIndex: Int,
         firstItemIndex: Int,
         lastItemIndex: Int,
         lastItemIndex: Int,
+        currentChapterVisible: Boolean,
         navigationId: Long,
         navigationId: Long,
     ) {
     ) {
         val state = _uiState.value
         val state = _uiState.value
@@ -1117,6 +1127,7 @@ class MangaReaderViewModel(
         when (mangaChapterSwitchDecision(
         when (mangaChapterSwitchDecision(
             currentChapterIndex = readerSession.state.value.chapterIndex,
             currentChapterIndex = readerSession.state.value.chapterIndex,
             visibleChapterIndex = item.chapterIndex,
             visibleChapterIndex = item.chapterIndex,
+            currentChapterVisible = currentChapterVisible,
         )) {
         )) {
             MangaChapterSwitch.NEXT,
             MangaChapterSwitch.NEXT,
             MangaChapterSwitch.PREVIOUS -> executeSession(
             MangaChapterSwitch.PREVIOUS -> executeSession(
@@ -1218,10 +1229,11 @@ class MangaReaderViewModel(
         disableScrollAnimation = settings.disableMangaScrollAnimation,
         disableScrollAnimation = settings.disableMangaScrollAnimation,
         disableCrossFade = settings.disableMangaCrossFade,
         disableCrossFade = settings.disableMangaCrossFade,
         disableClickScroll = settings.disableClickScroll,
         disableClickScroll = settings.disableClickScroll,
-        longPressEnabled = settings.longClick,
-        preDownloadCount = settings.preDownloadNum,
+            longPressEnabled = settings.longClick,
+            preDownloadCount = settings.preDownloadNum,
             chapterPrefetchCount = settings.chapterPrefetchCount,
             chapterPrefetchCount = settings.chapterPrefetchCount,
-        autoReadSpeed = settings.autoPageSpeed,
+            autoOfflineCache = settings.autoOfflineCache,
+            autoReadSpeed = settings.autoPageSpeed,
         volumeKeyPage = settings.volumeKeyPage,
         volumeKeyPage = settings.volumeKeyPage,
         reverseVolumeKeyPage = settings.reverseVolumeKeyPage,
         reverseVolumeKeyPage = settings.reverseVolumeKeyPage,
         hideMangaTitle = settings.hideTitle,
         hideMangaTitle = settings.hideTitle,

+ 18 - 5
app/src/main/java/io/legado/app/ui/book/manga/MangaSettingsPanel.kt

@@ -466,12 +466,25 @@ private fun ReaderSettingsContent(
     ) {
     ) {
         onIntent(MangaReaderIntent.UpdateSetting(MangaReaderSettingKey.PRE_DOWNLOAD, it))
         onIntent(MangaReaderIntent.UpdateSetting(MangaReaderSettingKey.PRE_DOWNLOAD, it))
     }
     }
-    SettingSlider(
-        stringResource(R.string.manga_reader_prefetch_chapters),
-        settings.chapterPrefetchCount,
-        0..3,
+    SettingSwitch(
+        stringResource(R.string.manga_reader_auto_offline_cache),
+        settings.autoOfflineCache,
     ) {
     ) {
-        onIntent(MangaReaderIntent.UpdateSetting(MangaReaderSettingKey.CHAPTER_PREFETCH, it))
+        onIntent(
+            MangaReaderIntent.UpdateSetting(
+                MangaReaderSettingKey.AUTO_OFFLINE_CACHE,
+                it.intValue,
+            )
+        )
+    }
+    if (settings.autoOfflineCache) {
+        SettingSlider(
+            stringResource(R.string.manga_reader_auto_cache_following_chapters),
+            settings.chapterPrefetchCount,
+            0..3,
+        ) {
+            onIntent(MangaReaderIntent.UpdateSetting(MangaReaderSettingKey.CHAPTER_PREFETCH, it))
+        }
     }
     }
     SettingSwitch(stringResource(R.string.manga_reader_pinch_zoom), !settings.disableScale) {
     SettingSwitch(stringResource(R.string.manga_reader_pinch_zoom), !settings.disableScale) {
         onIntent(
         onIntent(

+ 4 - 0
app/src/main/java/io/legado/app/ui/browser/WebViewRouteScreen.kt

@@ -159,6 +159,10 @@ fun WebViewRouteScreen(
                     modifier = Modifier.fillMaxSize(),
                     modifier = Modifier.fillMaxSize(),
                     onCreated = { createdWebView ->
                     onCreated = { createdWebView ->
                         createdWebView.apply {
                         createdWebView.apply {
+                            // Keep the verification page in an opaque WebView layer. A transparent WebView is
+                            // appropriate for RSS text, but Chromium repeatedly composites it during challenge
+                            // animations and causes the unstable flashing reported for manga sources.
+                            setBackgroundColor(android.graphics.Color.WHITE)
                             settings.apply {
                             settings.apply {
                                 mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
                                 mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
                                 domStorageEnabled = true
                                 domStorageEnabled = true

+ 2 - 1
app/src/main/res/values-zh-rCN/strings.xml

@@ -3212,7 +3212,8 @@
     <string name="manga_reader_copy_spread">复制双页</string>
     <string name="manga_reader_copy_spread">复制双页</string>
     <string name="manga_reader_cover_set">封面已更新</string>
     <string name="manga_reader_cover_set">封面已更新</string>
     <string name="manga_reader_retry_failed_chapter">重试本章失败页面</string>
     <string name="manga_reader_retry_failed_chapter">重试本章失败页面</string>
-    <string name="manga_reader_prefetch_chapters">预加载章节</string>
+    <string name="manga_reader_auto_offline_cache">自动离线缓存</string>
+    <string name="manga_reader_auto_cache_following_chapters">自动缓存后续章节数</string>
     <string name="import_current_folder_as_manga">将当前目录作为漫画导入</string>
     <string name="import_current_folder_as_manga">将当前目录作为漫画导入</string>
     <string name="manga_reader_cache_current">缓存本章</string>
     <string name="manga_reader_cache_current">缓存本章</string>
     <string name="manga_reader_cache_following">缓存后续章节</string>
     <string name="manga_reader_cache_following">缓存后续章节</string>

+ 2 - 1
app/src/main/res/values/strings.xml

@@ -3217,7 +3217,8 @@
     <string name="manga_reader_copy_spread">Copy spread</string>
     <string name="manga_reader_copy_spread">Copy spread</string>
     <string name="manga_reader_cover_set">Cover updated</string>
     <string name="manga_reader_cover_set">Cover updated</string>
     <string name="manga_reader_retry_failed_chapter">Retry failed pages in chapter</string>
     <string name="manga_reader_retry_failed_chapter">Retry failed pages in chapter</string>
-    <string name="manga_reader_prefetch_chapters">Preload chapters</string>
+    <string name="manga_reader_auto_offline_cache">Automatically cache chapters offline</string>
+    <string name="manga_reader_auto_cache_following_chapters">Automatically cache following chapters</string>
     <string name="import_current_folder_as_manga">Import current folder as comic</string>
     <string name="import_current_folder_as_manga">Import current folder as comic</string>
     <string name="manga_reader_cache_current">Cache chapter</string>
     <string name="manga_reader_cache_current">Cache chapter</string>
     <string name="manga_reader_cache_following">Cache following</string>
     <string name="manga_reader_cache_following">Cache following</string>

+ 2 - 0
app/src/test/java/io/legado/app/data/repository/MangaSettingsMappingTest.kt

@@ -204,6 +204,7 @@ private fun mangaMappingSamples(): List<MangaSettings> {
         base.copy(disableMangaScale = false),
         base.copy(disableMangaScale = false),
         base.copy(disableMangaScrollAnimation = true),
         base.copy(disableMangaScrollAnimation = true),
         base.copy(disableMangaCrossFade = true),
         base.copy(disableMangaCrossFade = true),
+        base.copy(autoOfflineCache = true),
         base.copy(disableClickScroll = true),
         base.copy(disableClickScroll = true),
         base.copy(longClick = false),
         base.copy(longClick = false),
         base.copy(hideTitle = true),
         base.copy(hideTitle = true),
@@ -230,6 +231,7 @@ private fun MangaSettings.expectedPrefMap(): Map<String, Any?> = mapOf(
     PreferKey.mangaScrollMode to scrollMode,
     PreferKey.mangaScrollMode to scrollMode,
     PreferKey.mangaPreDownloadNum to preDownloadNum,
     PreferKey.mangaPreDownloadNum to preDownloadNum,
     PreferKey.mangaChapterPrefetchCount to chapterPrefetchCount,
     PreferKey.mangaChapterPrefetchCount to chapterPrefetchCount,
+    PreferKey.mangaAutoOfflineCache to autoOfflineCache,
     PreferKey.mangaAutoPageSpeed to autoPageSpeed,
     PreferKey.mangaAutoPageSpeed to autoPageSpeed,
     PreferKey.mangaFooterConfig to footerConfig,
     PreferKey.mangaFooterConfig to footerConfig,
     PreferKey.disableClickScroll to disableClickScroll,
     PreferKey.disableClickScroll to disableClickScroll,

+ 24 - 0
app/src/test/java/io/legado/app/model/CacheBookModelResumeTest.kt

@@ -4,6 +4,7 @@ import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSource
 import io.legado.app.model.cache.CacheDownloadRequest
 import io.legado.app.model.cache.CacheDownloadRequest
+import io.legado.app.model.cache.CacheDownloadSource
 import io.legado.app.model.cache.CacheDownloadStateStore
 import io.legado.app.model.cache.CacheDownloadStateStore
 import io.legado.app.model.cache.ChapterSelection
 import io.legado.app.model.cache.ChapterSelection
 import org.junit.Assert.assertEquals
 import org.junit.Assert.assertEquals
@@ -93,6 +94,29 @@ class CacheBookModelResumeTest {
         assertEquals(1, model.diagnostics().waitingChapterCount)
         assertEquals(1, model.diagnostics().waitingChapterCount)
     }
     }
 
 
+    @Test
+    fun readerPreloadWhileBookPaused_keepsTheBookPaused() {
+        val model = newModel()
+        model.addRequest(
+            CacheDownloadRequest(
+                bookUrl = BOOK_URL,
+                selection = ChapterSelection.Range(0, 5),
+            )
+        )
+        assertTrue(model.pause())
+
+        model.addRequest(
+            CacheDownloadRequest(
+                bookUrl = BOOK_URL,
+                selection = ChapterSelection.Single(6),
+                source = CacheDownloadSource.ReadPreload,
+            )
+        )
+
+        assertTrue(model.isPaused())
+        assertFalse(model.hasLaunchableChapters())
+    }
+
     @Test
     @Test
     fun resumeBook_restoresAllPausedChaptersOnlyForThatModel() {
     fun resumeBook_restoresAllPausedChaptersOnlyForThatModel() {
         val model = newModel()
         val model = newModel()

+ 15 - 0
app/src/test/java/io/legado/app/ui/book/manga/MangaReaderInteractionTest.kt

@@ -145,6 +145,7 @@ class MangaReaderInteractionTest {
             mangaChapterSwitchDecision(
             mangaChapterSwitchDecision(
                 currentChapterIndex = 5,
                 currentChapterIndex = 5,
                 visibleChapterIndex = 6,
                 visibleChapterIndex = 6,
+                currentChapterVisible = false,
             ),
             ),
         )
         )
     }
     }
@@ -156,6 +157,7 @@ class MangaReaderInteractionTest {
             mangaChapterSwitchDecision(
             mangaChapterSwitchDecision(
                 currentChapterIndex = 5,
                 currentChapterIndex = 5,
                 visibleChapterIndex = 4,
                 visibleChapterIndex = 4,
+                currentChapterVisible = false,
             ),
             ),
         )
         )
     }
     }
@@ -167,6 +169,19 @@ class MangaReaderInteractionTest {
             mangaChapterSwitchDecision(
             mangaChapterSwitchDecision(
                 currentChapterIndex = 5,
                 currentChapterIndex = 5,
                 visibleChapterIndex = 5,
                 visibleChapterIndex = 5,
+                currentChapterVisible = true,
+            ),
+        )
+    }
+
+    @Test
+    fun `adjacent prefetched chapter cannot replace the chapter still on screen`() {
+        assertEquals(
+            MangaChapterSwitch.NONE,
+            mangaChapterSwitchDecision(
+                currentChapterIndex = 5,
+                visibleChapterIndex = 6,
+                currentChapterVisible = true,
             ),
             ),
         )
         )
     }
     }