diff --git a/app/src/main/java/io/legado/app/data/repository/SearchContentRepository.kt b/app/src/main/java/io/legado/app/data/repository/SearchContentRepository.kt index a58904056..ef53d7528 100644 --- a/app/src/main/java/io/legado/app/data/repository/SearchContentRepository.kt +++ b/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 { - val position: MutableList = mutableListOf() + private fun searchPosition(content: String, pattern: String, regexReplace: Boolean): List { + val positions: MutableList = 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 { 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, + ) + } diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt index 789309ba6..aa948b042 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookController.kt +++ b/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( diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt index 8744d8e32..2503b6507 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt @@ -2202,55 +2202,102 @@ class ReadBookViewModel( ): Array { val pages = textChapter.pages val content = textChapter.getContent() - val queryLength = query.length + if (pages.isEmpty()) return arrayOf(0, 0, 0, 0, 0) + + 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) + + val pageIndex = textChapter.getPageIndexByCharIndex(contentPosition) + .coerceIn(0, pages.lastIndex) + val currentPage = pages[pageIndex] + 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) + } + + 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 + } var count = 0 - var index = content.indexOf(query) - while (count != searchResult.resultCountWithinChapter) { - index = content.indexOf(query, index + queryLength) + var index = content.indexOf(query, ignoreCase = true) + while (count != searchResult.resultCountWithinChapter && index >= 0) { + index = content.indexOf(query, index + query.length, ignoreCase = true) count += 1 } - val contentPosition = index - 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 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 currentLine = currentPage.lines[lineIndex] - var curLineLength = currentLine.text.length - if (currentLine.isParagraphEnd) curLineLength++ - length -= curLineLength - - val charIndex = contentPosition - length - var addLine = 0 - var charIndex2 = 0 - if ((charIndex + queryLength) > curLineLength) { - addLine = 1 - charIndex2 = charIndex + queryLength - curLineLength - 1 - } - if ((lineIndex + addLine + 1) > currentPage.lines.size) { - addLine = -1 - charIndex2 = charIndex + queryLength - curLineLength - 1 - } - return arrayOf(pageIndex, lineIndex, charIndex, addLine, charIndex2) + 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 + } + 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, diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt index ef709ffaf..6f78393f8 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt +++ b/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)) } /** diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt index 7b90fd2ec..8fcb1e904 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchResult.kt +++ b/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 ) {