Просмотр исходного кода

fix: 修复阅读搜索定位和双页同步问题

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

+ 18 - 12
app/src/main/java/io/legado/app/data/repository/SearchContentRepository.kt

@@ -86,10 +86,10 @@ class SearchContentRepository {
             book, chapter, chapterContent, useReplace = replaceEnabled
         ).toString()
 
-        val positions = searchPosition(mContent, query, regexReplace)
+        val matches = searchPosition(mContent, query, regexReplace)
 
-        positions.forEachIndexed { index, position ->
-            val construct = getResultAndQueryIndex(mContent, position, query)
+        matches.forEachIndexed { index, match ->
+            val construct = getResultAndQueryIndex(mContent, match.position, match.length)
             val result = SearchResult(
                 resultCountWithinChapter = index,
                 resultText = construct.second,
@@ -97,7 +97,8 @@ class SearchContentRepository {
                 query = query,
                 chapterIndex = chapter.index,
                 queryIndexInResult = construct.first,
-                queryIndexInChapter = position,
+                queryIndexInChapter = match.position,
+                matchLength = match.length,
                 isRegex = regexReplace
             )
             searchResultsWithinChapter.add(result)
@@ -105,34 +106,34 @@ class SearchContentRepository {
         return searchResultsWithinChapter
     }
 
-    private fun searchPosition(content: String, pattern: String, regexReplace: Boolean): List<Int> {
-        val position: MutableList<Int> = mutableListOf()
+    private fun searchPosition(content: String, pattern: String, regexReplace: Boolean): List<SearchMatch> {
+        val positions: MutableList<SearchMatch> = mutableListOf()
         if (regexReplace) { // 正则表达式搜索
             try {
                 Regex(pattern).findAll(content).forEach { match ->
-                    position.add(match.range.first)
+                    positions.add(SearchMatch(match.range.first, match.value.length))
                 }
             } catch (e: Exception) {
-                return position
+                return positions
             }
         } else {
             var index = content.indexOf(pattern)
             while (index >= 0) {
-                position.add(index)
+                positions.add(SearchMatch(index, pattern.length))
                 index = content.indexOf(pattern, index + pattern.length)
             }
         }
-        return position
+        return positions
     }
 
     private fun getResultAndQueryIndex(
         content: String,
         queryIndexInContent: Int,
-        query: String
+        matchLength: Int
     ): Pair<Int, String> {
         val length = 12
         var po1 = queryIndexInContent - length
-        var po2 = queryIndexInContent + query.length + length
+        var po2 = queryIndexInContent + matchLength + length
         if (po1 < 0) {
             po1 = 0
         }
@@ -144,4 +145,9 @@ class SearchContentRepository {
         return queryIndexInResult to newText
     }
 
+    private data class SearchMatch(
+        val position: Int,
+        val length: Int,
+    )
+
 }

+ 2 - 9
app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt

@@ -713,15 +713,8 @@ class ReadBookController(
                         val pos = viewModel.searchResultPositions(tc, effect.result, query)
                         val lineIndex = pos[1]
                         val charIndex = pos[2]
-                        val addLine = pos[3]
-                        val charIndex2 = pos[4]
-                        val queryLength = query.length
-                        val endLineIndex = lineIndex + addLine.coerceAtLeast(0)
-                        val endCharIndex = if (addLine == 0) {
-                            charIndex + queryLength
-                        } else {
-                            charIndex2 + 1
-                        }
+                        val endLineIndex = pos[3]
+                        val endCharIndex = pos[4]
                         activity.lifecycleScope.launch(Main) {
                             navigatePageByPos(tc, pos[0])
                             markSearchResultOnPage(

+ 88 - 48
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -2202,55 +2202,102 @@ class ReadBookViewModel(
     ): Array<Int> {
         val pages = textChapter.pages
         val content = textChapter.getContent()
-        val queryLength = query.length
+        if (pages.isEmpty()) return arrayOf(0, 0, 0, 0, 0)
 
-        var count = 0
-        var index = content.indexOf(query)
-        while (count != searchResult.resultCountWithinChapter) {
-            index = content.indexOf(query, index + queryLength)
-            count += 1
-        }
-        val contentPosition = index
+        val contentPosition = searchResult.queryIndexInChapter
+            .takeIf { it in content.indices && searchResultMatchesAt(content, it, searchResult, query) }
+            ?: findSearchResultPosition(content, searchResult, query)
         if (contentPosition < 0) return arrayOf(0, 0, 0, 0, 0)
-        var pageIndex = 0
-        var length = pages[pageIndex].text.length
-        while (length < contentPosition && pageIndex + 1 < pages.size) {
-            pageIndex += 1
-            length += pages[pageIndex].text.length
-        }
 
+        val pageIndex = textChapter.getPageIndexByCharIndex(contentPosition)
+            .coerceIn(0, pages.lastIndex)
         val currentPage = pages[pageIndex]
-        val curTextLines = currentPage.lines
-        var lineIndex = 0
-        var curLine = curTextLines[lineIndex]
-        length = length - currentPage.text.length + curLine.text.length
-        if (curLine.isParagraphEnd) length++
-        while (length < contentPosition && lineIndex + 1 < curTextLines.size) {
-            lineIndex += 1
-            curLine = curTextLines[lineIndex]
-            length += curLine.text.length
-            if (curLine.isParagraphEnd) length++
-        }
+        val matchLength = searchResult.matchLength.takeIf { it > 0 } ?: query.length
+        val endPosition = (contentPosition + matchLength).coerceAtMost(content.length)
+        val start = linePositionFor(currentPage, contentPosition, preferPreviousAtBoundary = false)
+        val end = linePositionFor(currentPage, endPosition, preferPreviousAtBoundary = true)
+        return arrayOf(pageIndex, start.lineIndex, start.charIndex, end.lineIndex, end.charIndex)
+    }
 
-        val currentLine = currentPage.lines[lineIndex]
-        var curLineLength = currentLine.text.length
-        if (currentLine.isParagraphEnd) curLineLength++
-        length -= curLineLength
+    private fun findSearchResultPosition(
+        content: String,
+        searchResult: SearchResult,
+        query: String,
+    ): Int {
+        if (query.isEmpty()) return -1
+        if (searchResult.isRegex) {
+            return runCatching {
+                Regex(query).findAll(content)
+                    .drop(searchResult.resultCountWithinChapter)
+                    .firstOrNull()
+                    ?.range
+                    ?.first
+            }.getOrNull() ?: -1
+        }
 
-        val charIndex = contentPosition - length
-        var addLine = 0
-        var charIndex2 = 0
-        if ((charIndex + queryLength) > curLineLength) {
-            addLine = 1
-            charIndex2 = charIndex + queryLength - curLineLength - 1
+        var count = 0
+        var index = content.indexOf(query, ignoreCase = true)
+        while (count != searchResult.resultCountWithinChapter && index >= 0) {
+            index = content.indexOf(query, index + query.length, ignoreCase = true)
+            count += 1
         }
-        if ((lineIndex + addLine + 1) > currentPage.lines.size) {
-            addLine = -1
-            charIndex2 = charIndex + queryLength - curLineLength - 1
+        return index
+    }
+
+    private fun searchResultMatchesAt(
+        content: String,
+        position: Int,
+        searchResult: SearchResult,
+        query: String,
+    ): Boolean {
+        if (query.isEmpty()) return false
+        if (searchResult.isRegex) {
+            return runCatching {
+                Regex(query).find(content, position)?.range?.first == position
+            }.getOrDefault(false)
+        }
+        return position + query.length <= content.length &&
+                content.regionMatches(position, query, 0, query.length, ignoreCase = true)
+    }
+
+    private fun linePositionFor(
+        page: TextPage,
+        chapterPosition: Int,
+        preferPreviousAtBoundary: Boolean,
+    ): SearchLinePosition {
+        if (page.lines.isEmpty()) return SearchLinePosition(0, 0)
+        var fallbackIndex = 0
+        page.lines.forEachIndexed { index, line ->
+            val lineEnd = line.chapterPosition + line.text.length +
+                    if (line.isParagraphEnd) 1 else 0
+            if (chapterPosition < line.chapterPosition) {
+                return@forEachIndexed
+            }
+            if (preferPreviousAtBoundary && chapterPosition == line.chapterPosition && index > 0) {
+                val previousLine = page.lines[index - 1]
+                return SearchLinePosition(index - 1, previousLine.text.length)
+            }
+            if (chapterPosition <= lineEnd) {
+                return SearchLinePosition(
+                    lineIndex = index,
+                    charIndex = (chapterPosition - line.chapterPosition).coerceIn(0, line.text.length),
+                )
+            }
+            fallbackIndex = index
         }
-        return arrayOf(pageIndex, lineIndex, charIndex, addLine, charIndex2)
+        val lineIndex = fallbackIndex
+        val line = page.lines[lineIndex]
+        return SearchLinePosition(
+            lineIndex = lineIndex,
+            charIndex = (chapterPosition - line.chapterPosition).coerceIn(0, line.text.length),
+        )
     }
 
+    private data class SearchLinePosition(
+        val lineIndex: Int,
+        val charIndex: Int,
+    )
+
     /**
      * Compute the search result position and emit [ReadBookEffect.NavigateToSearchResult]
      * so the Controller can navigate and highlight.
@@ -2264,15 +2311,8 @@ class ReadBookViewModel(
             val pos = searchResultPositions(textChapter, result, query)
             val lineIndex = pos[1]
             val charIndex = pos[2]
-            val addLine = pos[3]
-            val charIndex2 = pos[4]
-            val queryLength = query.length
-            val endLineIndex = lineIndex + addLine.coerceAtLeast(0)
-            val endCharIndex = if (addLine == 0) {
-                charIndex + queryLength
-            } else {
-                charIndex2 + 1
-            }
+            val endLineIndex = pos[3]
+            val endCharIndex = pos[4]
             _effects.tryEmit(
                 ReadBookEffect.NavigateToSearchResult(
                     result = result,

+ 1 - 1
app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt

@@ -1079,7 +1079,7 @@ object ChapterProvider {
         viewWidth = width
         viewHeight = height
         upLayout()
-        postEvent(EventBus.UP_CONFIG, arrayListOf(5))
+        postEvent(EventBus.UP_CONFIG, arrayListOf(10, 5))
     }
 
     /**

+ 1 - 0
app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt

@@ -21,6 +21,7 @@ data class SearchResult(
     val pageIndex: Int = 0,
     val queryIndexInResult: Int = 0,
     val queryIndexInChapter: Int = 0,
+    val matchLength: Int = query.length,
     val isRegex: Boolean = false,
     val progressPercent: Float = 0f
 ) {