HapeLee 2 месяцев назад
Родитель
Сommit
b3ac3e9a8c

+ 35 - 0
app/src/main/java/io/legado/app/ui/book/manga/MangaReaderInteraction.kt

@@ -59,6 +59,41 @@ internal fun nextPageItemIndex(
     return null
 }
 
+/**
+ * Chooses the page that represents a Webtoon viewport.
+ *
+ * Normally the last visible page is a useful reading-progress anchor. At a chapter boundary it
+ * is not: a number of short pages from the adjacent chapter can be visible at once, so using the
+ * last one promotes the session directly to that chapter's final visible page. Once the current
+ * chapter has left the viewport, use the first visible page when entering a later chapter and
+ * the last visible page when entering an earlier one. Those are the pages adjacent to the
+ * boundary in reading order.
+ */
+internal fun mangaWebtoonFocusedPageIndex(
+    items: List<MangaReaderItemUi>,
+    visibleItemIndices: List<Int>,
+    currentChapterIndex: Int,
+): Int? {
+    val visiblePages = visibleItemIndices.mapNotNull { index ->
+        (items.getOrNull(index) as? MangaReaderItemUi.Page)?.let { index to it }
+    }
+    if (visiblePages.isEmpty()) return null
+    if (visiblePages.any { (_, page) -> page.chapterIndex == currentChapterIndex }) {
+        return visiblePages.last().first
+    }
+    return when {
+        visiblePages.first().second.chapterIndex > currentChapterIndex -> visiblePages.first().first
+        visiblePages.last().second.chapterIndex < currentChapterIndex -> visiblePages.last().first
+        else -> visiblePages.last().first
+    }
+}
+
+/** A programmatic position restore must not be overwritten by the old viewport's first callback. */
+internal fun acceptsMangaVisibleItem(
+    requestedItemIndex: Int?,
+    reportedItemIndex: Int,
+): Boolean = requestedItemIndex == null || requestedItemIndex == reportedItemIndex
+
 internal fun shouldExposeMangaPages(currentChapterFinished: Boolean): Boolean =
     currentChapterFinished
 

+ 6 - 1
app/src/main/java/io/legado/app/ui/book/manga/MangaReaderScreen.kt

@@ -472,12 +472,17 @@ private fun WebtoonMangaList(
     LaunchedEffect(listState, state.pages, state.navigationId) {
         snapshotFlow {
             val visibleItems = listState.layoutInfo.visibleItemsInfo
+            val visibleItemIndices = visibleItems.map { it.index }
             val currentChapterVisible = visibleItems.any { visibleItem ->
                 (state.pages.getOrNull(visibleItem.index) as? MangaReaderItemUi.Page)
                     ?.chapterIndex == state.chapterIndex
             }
             val firstItemIndex = visibleItems.firstOrNull()?.index
-            val focusedItemIndex = visibleItems.lastOrNull()?.index
+            val focusedItemIndex = mangaWebtoonFocusedPageIndex(
+                items = state.pages,
+                visibleItemIndices = visibleItemIndices,
+                currentChapterIndex = state.chapterIndex,
+            )
             if (firstItemIndex == null || focusedItemIndex == null) null
             else Triple(focusedItemIndex, firstItemIndex, currentChapterVisible)
         }

+ 17 - 10
app/src/main/java/io/legado/app/ui/book/manga/MangaReaderViewModel.kt

@@ -600,13 +600,18 @@ class MangaReaderViewModel(
                     } else old.scrollRequest,
                 )
             }
-            if (positionChanged && pendingExplicitChapterIndex != session.chapterIndex) updateVisibleItem(
-                itemIndex = targetIndex,
-                firstItemIndex = targetIndex,
-                lastItemIndex = targetIndex,
-                currentChapterVisible = true,
-                navigationId = _uiState.value.navigationId,
-            )
+            if (positionChanged && pendingExplicitChapterIndex != session.chapterIndex) {
+                // The session already owns this restored page. Notify it for adjacent-chapter
+                // loading without pretending the LazyColumn has reached the target: doing the
+                // latter clears scrollRequest and lets its stale page-0 callback overwrite the
+                // persisted progress before the restoration scroll runs.
+                executeSession(
+                    MangaSessionCommand.VisiblePageChanged(
+                        session.chapterIndex,
+                        session.pageIndex,
+                    )
+                )
+            }
         }
     }
 
@@ -1116,9 +1121,11 @@ class MangaReaderViewModel(
             }
             return
         }
-        val requestedItem = state.scrollRequest?.itemIndex
-            ?.let(state.pages::getOrNull) as? MangaReaderItemUi.Page
-        if (requestedItem != null && item.chapterIndex != requestedItem.chapterIndex) return
+        val requestedItemIndex = state.scrollRequest?.itemIndex
+        // LazyColumn first reports its retained/pre-scroll viewport. During a restore that page is
+        // commonly page 0, and accepting it overwrites the persisted target before scrollToItem
+        // reaches it. Only the requested item may complete a pending programmatic navigation.
+        if (!acceptsMangaVisibleItem(requestedItemIndex, itemIndex)) return
         if (pendingExplicitChapterIndex != null &&
             item.chapterIndex != readerSession.state.value.chapterIndex
         ) return

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

@@ -132,6 +132,66 @@ class MangaReaderInteractionTest {
         )
     }
 
+    @Test
+    fun `webtoon enters later chapter at its first visible page not its last`() {
+        val items = listOf(
+            page(8, chapter = 20),
+            MangaReaderItemUi.ChapterTransition(
+                key = "transition",
+                direction = MangaChapterTransitionDirection.NEXT,
+                targetChapterIndex = 21,
+                currentChapterName = "20",
+                targetChapterName = "21",
+                targetStatus = MangaChapterTransitionStatus.READY,
+            ),
+            page(0, chapter = 21),
+            page(1, chapter = 21),
+            page(2, chapter = 21),
+        )
+
+        assertEquals(
+            2,
+            mangaWebtoonFocusedPageIndex(
+                items = items,
+                visibleItemIndices = listOf(1, 2, 3, 4),
+                currentChapterIndex = 20,
+            ),
+        )
+    }
+
+    @Test
+    fun `webtoon enters earlier chapter at its last visible page`() {
+        val items = listOf(
+            page(7, chapter = 20),
+            page(8, chapter = 20),
+            MangaReaderItemUi.ChapterTransition(
+                key = "transition",
+                direction = MangaChapterTransitionDirection.NEXT,
+                targetChapterIndex = 21,
+                currentChapterName = "20",
+                targetChapterName = "21",
+                targetStatus = MangaChapterTransitionStatus.READY,
+            ),
+            page(0, chapter = 21),
+        )
+
+        assertEquals(
+            1,
+            mangaWebtoonFocusedPageIndex(
+                items = items,
+                visibleItemIndices = listOf(0, 1, 2),
+                currentChapterIndex = 21,
+            ),
+        )
+    }
+
+    @Test
+    fun `pending restored position rejects old viewport callback`() {
+        assertFalse(acceptsMangaVisibleItem(requestedItemIndex = 14, reportedItemIndex = 0))
+        assertTrue(acceptsMangaVisibleItem(requestedItemIndex = 14, reportedItemIndex = 14))
+        assertTrue(acceptsMangaVisibleItem(requestedItemIndex = null, reportedItemIndex = 0))
+    }
+
     @Test
     fun `adjacent chapter callbacks stay hidden until target chapter finishes`() {
         assertFalse(shouldExposeMangaPages(currentChapterFinished = false))