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 6e3457826..b2b302268 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 @@ -122,6 +122,7 @@ class SearchContentRepository { matches.forEachIndexed { index, match -> val construct = getResultAndQueryIndex(mContent, match.position, match.length) val result = SearchResult( + bookUrl = book.bookUrl, resultCountWithinChapter = index, resultText = construct.second, chapterTitle = chapter.title, 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 72c02227a..8342a8434 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 @@ -717,7 +717,7 @@ class ReadBookController( // Chapter not loaded — open it, then mark in the success callback ReadBook.openChapter( effect.chapterIndex, - effect.result.queryIndexInChapter + 0 ) { val tc = ReadBook.curTextChapter ?: return@openChapter val query = effect.result.query diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt index e8a4aa9a1..ef879ee04 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt @@ -31,6 +31,7 @@ import io.legado.app.R import io.legado.app.constant.AppLog import io.legado.app.constant.ReadMenuBlurMode import io.legado.app.help.IntentHelp +import io.legado.app.model.ReadBook import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.book.read.page.ContentTextView import io.legado.app.ui.book.read.page.ReadView @@ -383,6 +384,10 @@ fun ReadBookRouteScreen( lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) { SearchContentResult.results.collect { result -> effectsReady.await() + if (result.bookUrl != ReadBook.book?.bookUrl) { + SearchContentResult.resetReplayCache() + return@collect + } viewModel.onIntent( ReadBookIntent.SetSearchResults(result.searchResults, result.index, result.query) ) 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 e3110f726..2f720bd3e 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 @@ -2241,50 +2241,21 @@ class ReadBookViewModel( return arrayOf(-1, 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 - } - if (length < contentPosition) return arrayOf(-1, 0, 0, 0, 0, 0) - - val currentPage = pages[pageIndex] - val lines = currentPage.lines - if (lines.isEmpty()) return arrayOf(-1, 0, 0, 0, 0, 0) - - var lineIndex = 0 - var currentLine = lines[lineIndex] - length = length - currentPage.text.length + currentLine.text.length - if (currentLine.isParagraphEnd) length++ - while (length <= contentPosition && lineIndex + 1 < lines.size) { - lineIndex += 1 - currentLine = lines[lineIndex] - length += currentLine.text.length - if (currentLine.isParagraphEnd) length++ - } - - var currentLineLength = currentLine.text.length - if (currentLine.isParagraphEnd) currentLineLength++ - length -= currentLineLength - - val charIndex = contentPosition - length - var addLine = 0 - var charIndex2 = 0 - if ((charIndex + queryLength) > currentLineLength) { - addLine = 1 - charIndex2 = charIndex + queryLength - currentLineLength - 1 - } - if ((lineIndex + addLine + 1) > currentPage.lines.size) { - addLine = -1 - charIndex2 = charIndex + queryLength - currentLineLength - 1 - } - return when (addLine) { - 0 -> arrayOf(pageIndex, lineIndex, charIndex, 0, lineIndex, charIndex + queryLength - 1) - 1 -> arrayOf(pageIndex, lineIndex, charIndex, 0, lineIndex + 1, charIndex2) - -1 -> arrayOf(pageIndex, lineIndex, charIndex, 1, 0, charIndex2) - else -> arrayOf(pageIndex, lineIndex, charIndex, 0, lineIndex, charIndex) - } + val start = findSearchTextPoint(pages, contentPosition, preferPreviousLine = false) + ?: return arrayOf(-1, 0, 0, 0, 0, 0) + val end = findSearchTextPoint( + pages, + contentPosition + queryLength - 1, + preferPreviousLine = true + ) ?: start + return arrayOf( + start.pageIndex, + start.lineIndex, + start.charIndex, + end.pageIndex - start.pageIndex, + end.lineIndex, + end.charIndex + ) } private fun findSearchResultMatch( @@ -2293,6 +2264,26 @@ class ReadBookViewModel( query: String, ): Pair? { if (query.isEmpty()) return null + val directLength = if (searchResult.matchLength > 0) searchResult.matchLength else query.length + val directIndex = searchResult.queryIndexInChapter + if (directIndex >= 0 && directIndex + directLength <= content.length) { + val directMatch = if (searchResult.isRegex) { + runCatching { + Regex(query).matches(content.substring(directIndex, directIndex + directLength)) + }.getOrDefault(false) + } else { + content.regionMatches( + directIndex, + query, + 0, + query.length, + ignoreCase = false + ) + } + if (directMatch) { + return directIndex to directLength + } + } if (searchResult.isRegex) { return runCatching { Regex(query).findAll(content) @@ -2311,6 +2302,43 @@ class ReadBookViewModel( return index.takeIf { it >= 0 }?.let { it to query.length } } + private fun findSearchTextPoint( + pages: List, + contentPosition: Int, + preferPreviousLine: Boolean, + ): SearchTextPoint? { + var fallback: SearchTextPoint? = null + pages.forEachIndexed { pageIndex, page -> + page.lines.forEachIndexed { lineIndex, line -> + if (line.columns.isEmpty()) return@forEachIndexed + val lineStart = line.chapterPosition + val lineEndExclusive = lineStart + line.charSize + if (contentPosition in lineStart until lineEndExclusive) { + return SearchTextPoint( + pageIndex = pageIndex, + lineIndex = lineIndex, + charIndex = (contentPosition - lineStart).coerceIn(0, line.columns.lastIndex) + ) + } + if (preferPreviousLine && line.isParagraphEnd && contentPosition == lineEndExclusive) { + return SearchTextPoint( + pageIndex = pageIndex, + lineIndex = lineIndex, + charIndex = line.columns.lastIndex + ) + } + if (contentPosition >= lineEndExclusive) { + fallback = SearchTextPoint( + pageIndex = pageIndex, + lineIndex = lineIndex, + charIndex = line.columns.lastIndex + ) + } + } + } + return fallback + } + /** * Compute the search result position and emit [ReadBookEffect.NavigateToSearchResult] * so the Controller can navigate and highlight. @@ -3572,6 +3600,12 @@ private const val TOOL_BUTTON_PREFS = "tool_button_config" private const val TOOL_BUTTON_KEY = "tool_buttons" private const val DEFAULT_ENABLED_BUTTON_COUNT = 5 +private data class SearchTextPoint( + val pageIndex: Int, + val lineIndex: Int, + val charIndex: Int, +) + private fun Int.coerceSearchResultIndex(resultSize: Int): Int { return if (resultSize <= 0) 0 else coerceIn(0, resultSize - 1) } diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentResult.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentResult.kt index c996e410b..6fac850b9 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentResult.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentResult.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.book.searchContent +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -13,6 +14,7 @@ import kotlinx.coroutines.flow.asSharedFlow object SearchContentResult { data class Result( + val bookUrl: String, val searchResults: List, val index: Int, val query: String, @@ -25,6 +27,7 @@ object SearchContentResult { _results.tryEmit(result) } + @OptIn(ExperimentalCoroutinesApi::class) fun resetReplayCache() { _results.resetReplayCache() } diff --git a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt index 31b08a00e..324f9eab8 100644 --- a/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentViewModel.kt @@ -195,6 +195,7 @@ class SearchContentViewModel( if (index < 0) return false SearchContentResult.emitResult( SearchContentResult.Result( + bookUrl = bookUrl, searchResults = results, index = index, query = searchResult.query 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 8fcb1e904..eee4acc5d 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 @@ -11,6 +11,7 @@ import android.text.style.UnderlineSpan import io.legado.app.help.config.AppConfig data class SearchResult( + val bookUrl: String = "", val resultCount: Int = 0, val resultCountWithinChapter: Int = 0, val resultText: String = "", diff --git a/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt b/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt index 464cac2bb..3ff1a7a2b 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt @@ -354,6 +354,7 @@ fun MainActivity.mainEntryProvider( entry { route -> val viewModel = koinViewModel( + key = route.bookUrl, parameters = { parametersOf(route) } ) SearchContentScreen(