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

This commit is contained in:
HapeLee
2026-06-12 13:54:44 +08:00
parent 77ff118171
commit b8ddb487eb
5 changed files with 114 additions and 74 deletions
@@ -86,10 +86,10 @@ class SearchContentRepository {
book, chapter, chapterContent, useReplace = replaceEnabled book, chapter, chapterContent, useReplace = replaceEnabled
).toString() ).toString()
val positions = searchPosition(mContent, query, regexReplace) val matches = searchPosition(mContent, query, regexReplace)
positions.forEachIndexed { index, position -> matches.forEachIndexed { index, match ->
val construct = getResultAndQueryIndex(mContent, position, query) val construct = getResultAndQueryIndex(mContent, match.position, match.length)
val result = SearchResult( val result = SearchResult(
resultCountWithinChapter = index, resultCountWithinChapter = index,
resultText = construct.second, resultText = construct.second,
@@ -97,7 +97,8 @@ class SearchContentRepository {
query = query, query = query,
chapterIndex = chapter.index, chapterIndex = chapter.index,
queryIndexInResult = construct.first, queryIndexInResult = construct.first,
queryIndexInChapter = position, queryIndexInChapter = match.position,
matchLength = match.length,
isRegex = regexReplace isRegex = regexReplace
) )
searchResultsWithinChapter.add(result) searchResultsWithinChapter.add(result)
@@ -105,34 +106,34 @@ class SearchContentRepository {
return searchResultsWithinChapter return searchResultsWithinChapter
} }
private fun searchPosition(content: String, pattern: String, regexReplace: Boolean): List<Int> { private fun searchPosition(content: String, pattern: String, regexReplace: Boolean): List<SearchMatch> {
val position: MutableList<Int> = mutableListOf() val positions: MutableList<SearchMatch> = mutableListOf()
if (regexReplace) { // 正则表达式搜索 if (regexReplace) { // 正则表达式搜索
try { try {
Regex(pattern).findAll(content).forEach { match -> Regex(pattern).findAll(content).forEach { match ->
position.add(match.range.first) positions.add(SearchMatch(match.range.first, match.value.length))
} }
} catch (e: Exception) { } catch (e: Exception) {
return position return positions
} }
} else { } else {
var index = content.indexOf(pattern) var index = content.indexOf(pattern)
while (index >= 0) { while (index >= 0) {
position.add(index) positions.add(SearchMatch(index, pattern.length))
index = content.indexOf(pattern, index + pattern.length) index = content.indexOf(pattern, index + pattern.length)
} }
} }
return position return positions
} }
private fun getResultAndQueryIndex( private fun getResultAndQueryIndex(
content: String, content: String,
queryIndexInContent: Int, queryIndexInContent: Int,
query: String matchLength: Int
): Pair<Int, String> { ): Pair<Int, String> {
val length = 12 val length = 12
var po1 = queryIndexInContent - length var po1 = queryIndexInContent - length
var po2 = queryIndexInContent + query.length + length var po2 = queryIndexInContent + matchLength + length
if (po1 < 0) { if (po1 < 0) {
po1 = 0 po1 = 0
} }
@@ -144,4 +145,9 @@ class SearchContentRepository {
return queryIndexInResult to newText return queryIndexInResult to newText
} }
private data class SearchMatch(
val position: Int,
val length: Int,
)
} }
@@ -713,15 +713,8 @@ class ReadBookController(
val pos = viewModel.searchResultPositions(tc, effect.result, query) val pos = viewModel.searchResultPositions(tc, effect.result, query)
val lineIndex = pos[1] val lineIndex = pos[1]
val charIndex = pos[2] val charIndex = pos[2]
val addLine = pos[3] val endLineIndex = pos[3]
val charIndex2 = pos[4] val endCharIndex = pos[4]
val queryLength = query.length
val endLineIndex = lineIndex + addLine.coerceAtLeast(0)
val endCharIndex = if (addLine == 0) {
charIndex + queryLength
} else {
charIndex2 + 1
}
activity.lifecycleScope.launch(Main) { activity.lifecycleScope.launch(Main) {
navigatePageByPos(tc, pos[0]) navigatePageByPos(tc, pos[0])
markSearchResultOnPage( markSearchResultOnPage(
@@ -2202,55 +2202,102 @@ class ReadBookViewModel(
): Array<Int> { ): Array<Int> {
val pages = textChapter.pages val pages = textChapter.pages
val content = textChapter.getContent() 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 count = 0
var index = content.indexOf(query) var index = content.indexOf(query, ignoreCase = true)
while (count != searchResult.resultCountWithinChapter) { while (count != searchResult.resultCountWithinChapter && index >= 0) {
index = content.indexOf(query, index + queryLength) index = content.indexOf(query, index + query.length, ignoreCase = true)
count += 1 count += 1
} }
val contentPosition = index return 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)
} }
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] * Compute the search result position and emit [ReadBookEffect.NavigateToSearchResult]
* so the Controller can navigate and highlight. * so the Controller can navigate and highlight.
@@ -2264,15 +2311,8 @@ class ReadBookViewModel(
val pos = searchResultPositions(textChapter, result, query) val pos = searchResultPositions(textChapter, result, query)
val lineIndex = pos[1] val lineIndex = pos[1]
val charIndex = pos[2] val charIndex = pos[2]
val addLine = pos[3] val endLineIndex = pos[3]
val charIndex2 = pos[4] val endCharIndex = pos[4]
val queryLength = query.length
val endLineIndex = lineIndex + addLine.coerceAtLeast(0)
val endCharIndex = if (addLine == 0) {
charIndex + queryLength
} else {
charIndex2 + 1
}
_effects.tryEmit( _effects.tryEmit(
ReadBookEffect.NavigateToSearchResult( ReadBookEffect.NavigateToSearchResult(
result = result, result = result,
@@ -1079,7 +1079,7 @@ object ChapterProvider {
viewWidth = width viewWidth = width
viewHeight = height viewHeight = height
upLayout() upLayout()
postEvent(EventBus.UP_CONFIG, arrayListOf(5)) postEvent(EventBus.UP_CONFIG, arrayListOf(10, 5))
} }
/** /**
@@ -21,6 +21,7 @@ data class SearchResult(
val pageIndex: Int = 0, val pageIndex: Int = 0,
val queryIndexInResult: Int = 0, val queryIndexInResult: Int = 0,
val queryIndexInChapter: Int = 0, val queryIndexInChapter: Int = 0,
val matchLength: Int = query.length,
val isRegex: Boolean = false, val isRegex: Boolean = false,
val progressPercent: Float = 0f val progressPercent: Float = 0f
) { ) {