fix:修复阅读搜索定位问题
This commit is contained in:
7 files changed
+195
-141
No files matched your search
@@ -7,6 +7,7 @@ import io.legado.app.help.book.BookHelp
|
|||||||
import io.legado.app.help.book.ContentProcessor
|
import io.legado.app.help.book.ContentProcessor
|
||||||
import io.legado.app.help.book.isLocal
|
import io.legado.app.help.book.isLocal
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
import io.legado.app.ui.book.searchContent.SearchResult
|
import io.legado.app.ui.book.searchContent.SearchResult
|
||||||
import io.legado.app.utils.ChineseUtils
|
import io.legado.app.utils.ChineseUtils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -21,8 +22,13 @@ class SearchContentRepository {
|
|||||||
private var lastSearchResults: List<SearchResult>? = null
|
private var lastSearchResults: List<SearchResult>? = null
|
||||||
private var lastQueryKey: String? = null
|
private var lastQueryKey: String? = null
|
||||||
|
|
||||||
fun getCache(bookUrl: String, query: String): List<SearchResult>? {
|
fun getCache(
|
||||||
val key = "$bookUrl-$query"
|
bookUrl: String,
|
||||||
|
query: String,
|
||||||
|
replaceEnabled: Boolean,
|
||||||
|
regexReplace: Boolean
|
||||||
|
): List<SearchResult>? {
|
||||||
|
val key = searchKey(bookUrl, query, replaceEnabled, regexReplace)
|
||||||
return if (lastQueryKey == key) lastSearchResults else null
|
return if (lastQueryKey == key) lastSearchResults else null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,10 +67,19 @@ class SearchContentRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastSearchResults = allResults
|
lastSearchResults = allResults
|
||||||
lastQueryKey = "${book.bookUrl}-$query"
|
lastQueryKey = searchKey(book.bookUrl, query, replaceEnabled, regexReplace)
|
||||||
emit(ArrayList(allResults))
|
emit(ArrayList(allResults))
|
||||||
}.flowOn(Dispatchers.Default)
|
}.flowOn(Dispatchers.Default)
|
||||||
|
|
||||||
|
private fun searchKey(
|
||||||
|
bookUrl: String,
|
||||||
|
query: String,
|
||||||
|
replaceEnabled: Boolean,
|
||||||
|
regexReplace: Boolean
|
||||||
|
): String {
|
||||||
|
return "$bookUrl-$query-$replaceEnabled-$regexReplace-${ReadBookConfig.titleMode}"
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun searchChapter(
|
private suspend fun searchChapter(
|
||||||
query: String,
|
query: String,
|
||||||
book: Book,
|
book: Book,
|
||||||
@@ -82,9 +97,25 @@ class SearchContentRepository {
|
|||||||
else -> chapter.title
|
else -> chapter.title
|
||||||
}
|
}
|
||||||
|
|
||||||
val mContent = contentProcessor.getContent(
|
val bodyContent = contentProcessor.getContent(
|
||||||
book, chapter, chapterContent, useReplace = replaceEnabled
|
book,
|
||||||
).toString()
|
chapter,
|
||||||
|
chapterContent,
|
||||||
|
includeTitle = false,
|
||||||
|
useReplace = replaceEnabled
|
||||||
|
)
|
||||||
|
val includeTitle = ReadBookConfig.titleMode != 2 ||
|
||||||
|
chapter.isVolume ||
|
||||||
|
bodyContent.textList.isEmpty()
|
||||||
|
val mContent = if (includeTitle) {
|
||||||
|
val title = chapter.getDisplayTitle(
|
||||||
|
contentProcessor.getTitleReplaceRules(),
|
||||||
|
useReplace = replaceEnabled && book.getUseReplaceRule()
|
||||||
|
)
|
||||||
|
listOf(title).plus(bodyContent.textList).joinToString("\n")
|
||||||
|
} else {
|
||||||
|
bodyContent.toString()
|
||||||
|
}
|
||||||
|
|
||||||
val matches = searchPosition(mContent, query, regexReplace)
|
val matches = searchPosition(mContent, query, regexReplace)
|
||||||
|
|
||||||
|
|||||||
@@ -258,6 +258,7 @@ sealed interface ReadBookIntent {
|
|||||||
data class SetSearchResults(val results: List<SearchResult>, val index: Int, val query: String? = null) : ReadBookIntent
|
data class SetSearchResults(val results: List<SearchResult>, val index: Int, val query: String? = null) : ReadBookIntent
|
||||||
data class SetSearchResultIndex(val index: Int) : ReadBookIntent
|
data class SetSearchResultIndex(val index: Int) : ReadBookIntent
|
||||||
data class SetShowingSearchResult(val value: Boolean) : ReadBookIntent
|
data class SetShowingSearchResult(val value: Boolean) : ReadBookIntent
|
||||||
|
data class NavigateSearchResultByOffset(val offset: Int) : ReadBookIntent
|
||||||
data class NavigateToSearchResult(val result: SearchResult, val index: Int) : ReadBookIntent
|
data class NavigateToSearchResult(val result: SearchResult, val index: Int) : ReadBookIntent
|
||||||
data object RestoreLastBookProgress : ReadBookIntent
|
data object RestoreLastBookProgress : ReadBookIntent
|
||||||
data object KeepCurrentBookProgress : ReadBookIntent
|
data object KeepCurrentBookProgress : ReadBookIntent
|
||||||
@@ -515,7 +516,11 @@ sealed interface ReadBookEffect {
|
|||||||
|
|
||||||
// ReadView operations (require Activity/View reference)
|
// ReadView operations (require Activity/View reference)
|
||||||
data class UpdateReadViewConfig(val actions: Set<ConfigUpdateAction>) : ReadBookEffect
|
data class UpdateReadViewConfig(val actions: Set<ConfigUpdateAction>) : ReadBookEffect
|
||||||
data class UpContent(val relativePosition: Int, val resetPageOffset: Boolean) : ReadBookEffect
|
data class UpContent(
|
||||||
|
val relativePosition: Int,
|
||||||
|
val resetPageOffset: Boolean,
|
||||||
|
val success: (() -> Unit)? = null,
|
||||||
|
) : ReadBookEffect
|
||||||
data class UpPageAnim(val upRecorder: Boolean) : ReadBookEffect
|
data class UpPageAnim(val upRecorder: Boolean) : ReadBookEffect
|
||||||
data object UpTime : ReadBookEffect
|
data object UpTime : ReadBookEffect
|
||||||
data class UpBattery(val level: Int) : ReadBookEffect
|
data class UpBattery(val level: Int) : ReadBookEffect
|
||||||
@@ -547,6 +552,7 @@ sealed interface ReadBookEffect {
|
|||||||
val pageIndex: Int,
|
val pageIndex: Int,
|
||||||
val lineIndex: Int,
|
val lineIndex: Int,
|
||||||
val startCharIndex: Int,
|
val startCharIndex: Int,
|
||||||
|
val endRelativePage: Int,
|
||||||
val endLineIndex: Int,
|
val endLineIndex: Int,
|
||||||
val endCharIndex: Int,
|
val endCharIndex: Int,
|
||||||
) : ReadBookEffect
|
) : ReadBookEffect
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ class ReadBookController(
|
|||||||
}
|
}
|
||||||
private val popupAction by lazy { PopupAction(activity) }
|
private val popupAction by lazy { PopupAction(activity) }
|
||||||
private var screenTimeOut: Long = 0
|
private var screenTimeOut: Long = 0
|
||||||
|
private var pendingSearchResultMark: IntArray? = null
|
||||||
// justInitData moved to ViewModel (set on InitData intent)
|
// justInitData moved to ViewModel (set on InitData intent)
|
||||||
|
|
||||||
val isAutoPage: Boolean get() = refs?.readView?.isAutoPage == true
|
val isAutoPage: Boolean get() = refs?.readView?.isAutoPage == true
|
||||||
@@ -599,6 +600,10 @@ class ReadBookController(
|
|||||||
|
|
||||||
is ReadBookEffect.UpContent -> {
|
is ReadBookEffect.UpContent -> {
|
||||||
refs?.readView?.upContent(effect.relativePosition, effect.resetPageOffset)
|
refs?.readView?.upContent(effect.relativePosition, effect.resetPageOffset)
|
||||||
|
effect.success?.invoke()
|
||||||
|
refs?.readView?.post {
|
||||||
|
consumePendingSearchResultMark()
|
||||||
|
}
|
||||||
if (effect.relativePosition == 0) onUnhandledEffect(ReadBookEffect.UpSeekBar)
|
if (effect.relativePosition == 0) onUnhandledEffect(ReadBookEffect.UpSeekBar)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,7 +616,10 @@ class ReadBookController(
|
|||||||
ReadBook.loadContent(false)
|
ReadBook.loadContent(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
is ReadBookEffect.CancelSelect -> refs?.readView?.cancelSelect()
|
is ReadBookEffect.CancelSelect -> {
|
||||||
|
pendingSearchResultMark = null
|
||||||
|
refs?.readView?.cancelSelect()
|
||||||
|
}
|
||||||
is ReadBookEffect.MenuImageStyleChanged -> refs?.readView?.upPageAnim()
|
is ReadBookEffect.MenuImageStyleChanged -> refs?.readView?.upPageAnim()
|
||||||
|
|
||||||
// ── Simple Activity-API effects ──
|
// ── Simple Activity-API effects ──
|
||||||
@@ -707,37 +715,35 @@ class ReadBookController(
|
|||||||
is ReadBookEffect.NavigateToSearchResult -> {
|
is ReadBookEffect.NavigateToSearchResult -> {
|
||||||
if (effect.pageIndex < 0) {
|
if (effect.pageIndex < 0) {
|
||||||
// Chapter not loaded — open it, then mark in the success callback
|
// Chapter not loaded — open it, then mark in the success callback
|
||||||
ReadBook.openChapter(effect.chapterIndex) {
|
ReadBook.openChapter(
|
||||||
|
effect.chapterIndex,
|
||||||
|
effect.result.queryIndexInChapter
|
||||||
|
) {
|
||||||
val tc = ReadBook.curTextChapter ?: return@openChapter
|
val tc = ReadBook.curTextChapter ?: return@openChapter
|
||||||
val query = effect.result.query
|
val query = effect.result.query
|
||||||
val pos = viewModel.searchResultPositions(tc, effect.result, query)
|
val pos = viewModel.searchResultPositions(tc, effect.result, query)
|
||||||
val lineIndex = pos[1]
|
if (pos[0] < 0) return@openChapter
|
||||||
val charIndex = pos[2]
|
|
||||||
val endLineIndex = pos[3]
|
|
||||||
val endCharIndex = pos[4]
|
|
||||||
activity.lifecycleScope.launch(Main) {
|
activity.lifecycleScope.launch(Main) {
|
||||||
navigatePageByPos(tc, pos[0])
|
markSearchResultAfterNavigation(
|
||||||
markSearchResultOnPage(
|
intArrayOf(pos[0], pos[1], pos[2], pos[3], pos[4], pos[5])
|
||||||
intArrayOf(
|
|
||||||
pos[0], lineIndex, charIndex, endLineIndex, endCharIndex
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Same chapter — navigate to page, then mark
|
// Same chapter — navigate to page, then mark
|
||||||
val tc = ReadBook.curTextChapter ?: return
|
val tc = ReadBook.curTextChapter ?: return
|
||||||
navigatePageByPos(tc, effect.pageIndex)
|
markSearchResultAfterNavigation(
|
||||||
markSearchResultOnPage(
|
|
||||||
intArrayOf(
|
intArrayOf(
|
||||||
effect.pageIndex, effect.lineIndex,
|
effect.pageIndex, effect.lineIndex,
|
||||||
effect.startCharIndex, effect.endLineIndex, effect.endCharIndex
|
effect.startCharIndex, effect.endRelativePage,
|
||||||
|
effect.endLineIndex, effect.endCharIndex
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is ReadBookEffect.ExitSearch -> {
|
is ReadBookEffect.ExitSearch -> {
|
||||||
|
pendingSearchResultMark = null
|
||||||
ReadBook.clearSearchResult()
|
ReadBook.clearSearchResult()
|
||||||
refs?.readView?.cancelSelect(clearSearchResult = true)
|
refs?.readView?.cancelSelect(clearSearchResult = true)
|
||||||
}
|
}
|
||||||
@@ -1105,33 +1111,46 @@ class ReadBookController(
|
|||||||
|
|
||||||
// ── Search result navigation helpers ─────────────────────────────
|
// ── Search result navigation helpers ─────────────────────────────
|
||||||
|
|
||||||
/**
|
private fun markSearchResultAfterNavigation(pos: IntArray) {
|
||||||
* Navigate to the page at [pageIndex] within the current chapter.
|
if (pos[0] < 0) return
|
||||||
*/
|
val readView = refs?.readView ?: return
|
||||||
private fun navigatePageByPos(textChapter: TextChapter, pageIndex: Int) {
|
pendingSearchResultMark = pos
|
||||||
val pagePos = textChapter.getReadLength(pageIndex)
|
ReadBook.skipToPage(pos[0]) {
|
||||||
if (ReadBook.durChapterPos != pagePos) {
|
readView.post {
|
||||||
ReadBook.durChapterPos = pagePos
|
consumePendingSearchResultMark()
|
||||||
ReadBook.callBack?.upContent()
|
}
|
||||||
ReadBook.callBack?.cancelSelect()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun consumePendingSearchResultMark(): Boolean {
|
||||||
|
val pos = pendingSearchResultMark ?: return false
|
||||||
|
val readView = refs?.readView ?: return false
|
||||||
|
if (ReadBook.durPageIndex != pos[0] || readView.curPage.textPage.index != pos[0]) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
pendingSearchResultMark = null
|
||||||
|
markSearchResultOnPage(pos)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark search result columns on the current page for highlighting.
|
* Mark search result columns on the current page for highlighting.
|
||||||
* @param pos array of [pageIndex, lineIndex, startCharIndex, endLineIndex, endCharIndex]
|
* @param pos array of [pageIndex, lineIndex, startCharIndex, endRelativePage, endLineIndex, endCharIndex]
|
||||||
*/
|
*/
|
||||||
private fun markSearchResultOnPage(pos: IntArray) {
|
private fun markSearchResultOnPage(pos: IntArray) {
|
||||||
val readView = refs?.readView ?: return
|
val readView = refs?.readView ?: return
|
||||||
val lineIndex = pos[1]
|
val lineIndex = pos[1]
|
||||||
val startCharIndex = pos[2]
|
val startCharIndex = pos[2]
|
||||||
val endLineIndex = pos[3]
|
val endRelativePage = pos[3]
|
||||||
val endCharIndex = pos[4]
|
val endLineIndex = pos[4]
|
||||||
|
val endCharIndex = pos[5]
|
||||||
|
ReadBook.clearSearchResult()
|
||||||
readView.cancelSelect(clearSearchResult = true)
|
readView.cancelSelect(clearSearchResult = true)
|
||||||
isSelectingSearchResult = true
|
isSelectingSearchResult = true
|
||||||
try {
|
try {
|
||||||
readView.curPage.selectStartMoveIndex(0, lineIndex, startCharIndex)
|
readView.curPage.selectStartMoveIndex(0, lineIndex, startCharIndex)
|
||||||
readView.curPage.selectEndMoveIndex(0, endLineIndex, endCharIndex)
|
readView.curPage.selectEndMoveIndex(endRelativePage, endLineIndex, endCharIndex)
|
||||||
|
readView.isTextSelected = true
|
||||||
} finally {
|
} finally {
|
||||||
isSelectingSearchResult = false
|
isSelectingSearchResult = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
@@ -99,6 +102,7 @@ fun ReadBookRouteScreen(
|
|||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val readPreferences by viewModel.readPreferences.collectAsStateWithLifecycle()
|
val readPreferences by viewModel.readPreferences.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val effectsReady = remember(viewModel) { CompletableDeferred<Unit>() }
|
val effectsReady = remember(viewModel) { CompletableDeferred<Unit>() }
|
||||||
val menuBackdrop = rememberLayerBackdrop()
|
val menuBackdrop = rememberLayerBackdrop()
|
||||||
val menuHazeState = remember { HazeState() }
|
val menuHazeState = remember { HazeState() }
|
||||||
@@ -375,18 +379,20 @@ fun ReadBookRouteScreen(
|
|||||||
|
|
||||||
// ── Search result collection (from Navigation3 search route) ──────
|
// ── Search result collection (from Navigation3 search route) ──────
|
||||||
|
|
||||||
LaunchedEffect(viewModel) {
|
LaunchedEffect(viewModel, lifecycleOwner) {
|
||||||
SearchContentResult.results.collect { result ->
|
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
effectsReady.await()
|
SearchContentResult.results.collect { result ->
|
||||||
viewModel.onIntent(
|
effectsReady.await()
|
||||||
ReadBookIntent.SetSearchResults(result.searchResults, result.index, result.query)
|
|
||||||
)
|
|
||||||
result.searchResults.getOrNull(result.index)?.let { searchResult ->
|
|
||||||
viewModel.onIntent(
|
viewModel.onIntent(
|
||||||
ReadBookIntent.NavigateToSearchResult(searchResult, result.index)
|
ReadBookIntent.SetSearchResults(result.searchResults, result.index, result.query)
|
||||||
)
|
)
|
||||||
|
result.searchResults.getOrNull(result.index)?.let { searchResult ->
|
||||||
|
viewModel.onIntent(
|
||||||
|
ReadBookIntent.NavigateToSearchResult(searchResult, result.index)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SearchContentResult.resetReplayCache()
|
||||||
}
|
}
|
||||||
SearchContentResult.resetReplayCache()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,14 +44,7 @@ fun ReadBookSearchBar(
|
|||||||
) {
|
) {
|
||||||
AppFloatingActionButton(
|
AppFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val prevIndex = currentIndex - 1
|
onIntent(ReadBookIntent.NavigateSearchResultByOffset(-1))
|
||||||
val result = state.searchResultList.getOrNull(prevIndex)
|
|
||||||
?: return@AppFloatingActionButton
|
|
||||||
onIntent(
|
|
||||||
ReadBookIntent.NavigateToSearchResult(
|
|
||||||
result, prevIndex
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
tooltipText = "上一个结果",
|
tooltipText = "上一个结果",
|
||||||
containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
|
containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
|
||||||
@@ -76,14 +69,7 @@ fun ReadBookSearchBar(
|
|||||||
) {
|
) {
|
||||||
AppFloatingActionButton(
|
AppFloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
val nextIndex = currentIndex + 1
|
onIntent(ReadBookIntent.NavigateSearchResultByOffset(1))
|
||||||
val result = state.searchResultList.getOrNull(nextIndex)
|
|
||||||
?: return@AppFloatingActionButton
|
|
||||||
onIntent(
|
|
||||||
ReadBookIntent.NavigateToSearchResult(
|
|
||||||
result, nextIndex
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
tooltipText = "下一个结果",
|
tooltipText = "下一个结果",
|
||||||
containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
|
containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
|
||||||
|
|||||||
@@ -258,6 +258,10 @@ class ReadBookViewModel(
|
|||||||
_uiState.update { it.copy(isShowingSearchResult = intent.value) }
|
_uiState.update { it.copy(isShowingSearchResult = intent.value) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
is ReadBookIntent.NavigateSearchResultByOffset -> {
|
||||||
|
navigateSearchResultByOffset(intent.offset)
|
||||||
|
}
|
||||||
|
|
||||||
is ReadBookIntent.NavigateToSearchResult -> {
|
is ReadBookIntent.NavigateToSearchResult -> {
|
||||||
ReadBook.saveCurrentBookProgress()
|
ReadBook.saveCurrentBookProgress()
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
@@ -1265,9 +1269,8 @@ class ReadBookViewModel(
|
|||||||
) {
|
) {
|
||||||
_uiState.update { syncFromReadBook(it) }
|
_uiState.update { syncFromReadBook(it) }
|
||||||
_effects.tryEmit(
|
_effects.tryEmit(
|
||||||
ReadBookEffect.UpContent(relativePosition, resetPageOffset)
|
ReadBookEffect.UpContent(relativePosition, resetPageOffset, success)
|
||||||
)
|
)
|
||||||
success?.invoke()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun upContentAwait(
|
override suspend fun upContentAwait(
|
||||||
@@ -1278,9 +1281,8 @@ class ReadBookViewModel(
|
|||||||
withContext(Main.immediate) {
|
withContext(Main.immediate) {
|
||||||
_uiState.update { syncFromReadBook(it) }
|
_uiState.update { syncFromReadBook(it) }
|
||||||
_effects.tryEmit(
|
_effects.tryEmit(
|
||||||
ReadBookEffect.UpContent(relativePosition, resetPageOffset)
|
ReadBookEffect.UpContent(relativePosition, resetPageOffset, success)
|
||||||
)
|
)
|
||||||
success?.invoke()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2216,102 +2218,86 @@ class ReadBookViewModel(
|
|||||||
): Array<Int> {
|
): Array<Int> {
|
||||||
val pages = textChapter.pages
|
val pages = textChapter.pages
|
||||||
val content = textChapter.getContent()
|
val content = textChapter.getContent()
|
||||||
if (pages.isEmpty()) return arrayOf(0, 0, 0, 0, 0)
|
if (pages.isEmpty()) return arrayOf(-1, 0, 0, 0, 0, 0)
|
||||||
|
|
||||||
val contentPosition = searchResult.queryIndexInChapter
|
val match = findSearchResultMatch(content, searchResult, query)
|
||||||
.takeIf { it in content.indices && searchResultMatchesAt(content, it, searchResult, query) }
|
?: return arrayOf(-1, 0, 0, 0, 0, 0)
|
||||||
?: findSearchResultPosition(content, searchResult, query)
|
val contentPosition = match.first
|
||||||
if (contentPosition < 0) return arrayOf(0, 0, 0, 0, 0)
|
val queryLength = match.second
|
||||||
|
if (contentPosition < 0 || queryLength <= 0) {
|
||||||
|
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 pageIndex = textChapter.getPageIndexByCharIndex(contentPosition)
|
|
||||||
.coerceIn(0, pages.lastIndex)
|
|
||||||
val currentPage = pages[pageIndex]
|
val currentPage = pages[pageIndex]
|
||||||
val matchLength = searchResult.matchLength.takeIf { it > 0 } ?: query.length
|
val lines = currentPage.lines
|
||||||
val endPosition = (contentPosition + matchLength).coerceAtMost(content.length)
|
if (lines.isEmpty()) return arrayOf(-1, 0, 0, 0, 0, 0)
|
||||||
val start = linePositionFor(currentPage, contentPosition, preferPreviousAtBoundary = false)
|
|
||||||
val end = linePositionFor(currentPage, endPosition, preferPreviousAtBoundary = true)
|
var lineIndex = 0
|
||||||
return arrayOf(pageIndex, start.lineIndex, start.charIndex, end.lineIndex, end.charIndex)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun findSearchResultPosition(
|
private fun findSearchResultMatch(
|
||||||
content: String,
|
content: String,
|
||||||
searchResult: SearchResult,
|
searchResult: SearchResult,
|
||||||
query: String,
|
query: String,
|
||||||
): Int {
|
): Pair<Int, Int>? {
|
||||||
if (query.isEmpty()) return -1
|
if (query.isEmpty()) return null
|
||||||
if (searchResult.isRegex) {
|
if (searchResult.isRegex) {
|
||||||
return runCatching {
|
return runCatching {
|
||||||
Regex(query).findAll(content)
|
Regex(query).findAll(content)
|
||||||
.drop(searchResult.resultCountWithinChapter)
|
.drop(searchResult.resultCountWithinChapter)
|
||||||
.firstOrNull()
|
.firstOrNull()
|
||||||
?.range
|
?.let { it.range.first to it.value.length }
|
||||||
?.first
|
}.getOrNull()
|
||||||
}.getOrNull() ?: -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var count = 0
|
var count = 0
|
||||||
var index = content.indexOf(query, ignoreCase = true)
|
var index = content.indexOf(query)
|
||||||
while (count != searchResult.resultCountWithinChapter && index >= 0) {
|
while (count != searchResult.resultCountWithinChapter && index >= 0) {
|
||||||
index = content.indexOf(query, index + query.length, ignoreCase = true)
|
index = content.indexOf(query, index + query.length)
|
||||||
count += 1
|
count += 1
|
||||||
}
|
}
|
||||||
return index
|
return index.takeIf { it >= 0 }?.let { it to query.length }
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
||||||
@@ -2325,8 +2311,9 @@ 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 endLineIndex = pos[3]
|
val endRelativePage = pos[3]
|
||||||
val endCharIndex = pos[4]
|
val endLineIndex = pos[4]
|
||||||
|
val endCharIndex = pos[5]
|
||||||
_effects.tryEmit(
|
_effects.tryEmit(
|
||||||
ReadBookEffect.NavigateToSearchResult(
|
ReadBookEffect.NavigateToSearchResult(
|
||||||
result = result,
|
result = result,
|
||||||
@@ -2334,6 +2321,7 @@ class ReadBookViewModel(
|
|||||||
pageIndex = pos[0],
|
pageIndex = pos[0],
|
||||||
lineIndex = lineIndex,
|
lineIndex = lineIndex,
|
||||||
startCharIndex = charIndex,
|
startCharIndex = charIndex,
|
||||||
|
endRelativePage = endRelativePage,
|
||||||
endLineIndex = endLineIndex,
|
endLineIndex = endLineIndex,
|
||||||
endCharIndex = endCharIndex,
|
endCharIndex = endCharIndex,
|
||||||
)
|
)
|
||||||
@@ -2347,6 +2335,7 @@ class ReadBookViewModel(
|
|||||||
pageIndex = -1,
|
pageIndex = -1,
|
||||||
lineIndex = 0,
|
lineIndex = 0,
|
||||||
startCharIndex = 0,
|
startCharIndex = 0,
|
||||||
|
endRelativePage = 0,
|
||||||
endLineIndex = 0,
|
endLineIndex = 0,
|
||||||
endCharIndex = 0,
|
endCharIndex = 0,
|
||||||
)
|
)
|
||||||
@@ -3450,6 +3439,18 @@ class ReadBookViewModel(
|
|||||||
_effects.tryEmit(ReadBookEffect.ExitSearch)
|
_effects.tryEmit(ReadBookEffect.ExitSearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun navigateSearchResultByOffset(offset: Int) {
|
||||||
|
val state = _uiState.value
|
||||||
|
val currentIndex = state.searchResultIndex.coerceSearchResultIndex(
|
||||||
|
state.searchResultList.size
|
||||||
|
)
|
||||||
|
val targetIndex = currentIndex + offset
|
||||||
|
val result = state.searchResultList.getOrNull(targetIndex) ?: return
|
||||||
|
ReadBook.saveCurrentBookProgress()
|
||||||
|
_uiState.update { it.copy(searchResultIndex = targetIndex) }
|
||||||
|
navigateToSearchResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
if (BaseReadAloudService.isRun && BaseReadAloudService.pause) {
|
if (BaseReadAloudService.isRun && BaseReadAloudService.pause) {
|
||||||
|
|||||||
@@ -81,7 +81,12 @@ class SearchContentViewModel(
|
|||||||
private fun initBook() {
|
private fun initBook() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val book = bookRepository.getBook(bookUrl)
|
val book = bookRepository.getBook(bookUrl)
|
||||||
val cachedResults = searchContentRepository.getCache(bookUrl, _searchQuery.value)
|
val cachedResults = searchContentRepository.getCache(
|
||||||
|
bookUrl,
|
||||||
|
_searchQuery.value,
|
||||||
|
_replaceEnabled.value,
|
||||||
|
_regexReplace.value
|
||||||
|
)
|
||||||
|
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
|||||||
Reference in new issue
Block a user