fix: 修复阅读搜索串书和定位问题

This commit is contained in:
HapeLee
2026-06-14 13:20:45 +08:00
parent 21f9409bd6
commit f0f7eaf5bc
8 changed files with 91 additions and 45 deletions
@@ -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,
@@ -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
@@ -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)
)
@@ -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<Int, Int>? {
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<TextPage>,
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)
}
@@ -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<SearchResult>,
val index: Int,
val query: String,
@@ -25,6 +27,7 @@ object SearchContentResult {
_results.tryEmit(result)
}
@OptIn(ExperimentalCoroutinesApi::class)
fun resetReplayCache() {
_results.resetReplayCache()
}
@@ -195,6 +195,7 @@ class SearchContentViewModel(
if (index < 0) return false
SearchContentResult.emitResult(
SearchContentResult.Result(
bookUrl = bookUrl,
searchResults = results,
index = index,
query = searchResult.query
@@ -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 = "",
@@ -354,6 +354,7 @@ fun MainActivity.mainEntryProvider(
entry<MainRouteSearchContent> { route ->
val viewModel = koinViewModel<SearchContentViewModel>(
key = route.bookUrl,
parameters = { parametersOf(route) }
)
SearchContentScreen(