From f612b7958e280477de97cb528ccdf0cac24a11cf Mon Sep 17 00:00:00 2001 From: HapeLee <63206378+HapeLee@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:35:14 +0800 Subject: [PATCH] =?UTF-8?q?[=E4=BC=98=E5=8C=96]=20=E4=BC=98=E5=8C=96Tab?= =?UTF-8?q?=E6=A0=8F=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../io/legado/app/ui/book/toc/TocScreen.kt | 3 +- .../io/legado/app/ui/book/toc/TocViewModel.kt | 87 +++++++++++++++++-- .../app/ui/replace/ReplaceRuleScreen.kt | 4 +- app/version.properties | 2 +- 4 files changed, 87 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt index fb052de37..09c46d2bd 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt @@ -421,7 +421,8 @@ fun TocScreen( selectedTabIndex = pagerState.currentPage, edgePadding = 0.dp, divider = {}, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), + containerColor = Color.Transparent ) { Tab( selected = pagerState.currentPage == 0, diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt index 82b4428cd..5b19b799e 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt @@ -12,10 +12,12 @@ import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.Bookmark +import io.legado.app.data.entities.ReplaceRule import io.legado.app.help.book.BookHelp import io.legado.app.help.book.ContentProcessor import io.legado.app.help.book.isLocal import io.legado.app.help.bookmark.BookmarkExporter +import io.legado.app.help.config.AppConfig import io.legado.app.model.CacheBook import io.legado.app.model.ReadBook import io.legado.app.model.localBook.LocalBook @@ -26,6 +28,7 @@ import io.legado.app.ui.widget.components.list.SelectableItem import io.legado.app.utils.toastOnUi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -97,6 +100,14 @@ private data class TocUiConfig( val isReverse: Boolean ) +private data class TitleCacheKey( + val bookUrl: String, + val useReplace: Boolean, + val rulesFingerprint: Int, + val chineseConverterType: Int, + val chapterCount: Int +) + data class FabAction(val icon: ImageVector, val label: String, val action: () -> Unit) @OptIn(ExperimentalCoroutinesApi::class) @@ -209,12 +220,17 @@ class TocViewModel( TocUiConfig(collapsed, useReplace, showWordCount, isReverse) } + private val titleReplaceCache = MutableStateFlow>(emptyMap()) + private var titleCacheJob: Job? = null + private var lastTitleCacheKey: TitleCacheKey? = null + override val rawDataFlow: Flow> = combine( bookState.filterNotNull().map { it.bookUrl }.distinctUntilChanged() .flatMapLatest { appDb.bookChapterDao.getChapterListFlow(it) }, downloadContextFlow, - uiConfigFlow - ) { originalChapters, downloadCtx, config -> + uiConfigFlow, + titleReplaceCache + ) { originalChapters, downloadCtx, config, cachedTitles -> val book = bookState.value ?: return@combine emptyList() val processedChapters = if (config.isReverse) { @@ -227,11 +243,19 @@ class TocViewModel( ContentProcessor.get(book.name, book.origin).getTitleReplaceRules() } else emptyList() + updateTitleReplaceCacheIfNeeded( + book = book, + chapters = processedChapters, + replaceRules = replaceRules, + useReplace = config.useReplace + ) + if (book.isLocal) { return@combine processedChapters.map { chapter -> + val baseTitle = chapter.getDisplayTitle(useReplace = false) TocDomainItem( chapter = chapter, - displayTitle = chapter.getDisplayTitle(replaceRules, true), + displayTitle = cachedTitles[chapter.index] ?: baseTitle, downloadState = DownloadState.LOCAL ) } @@ -251,9 +275,10 @@ class TocViewModel( else -> DownloadState.NONE } + val baseTitle = chapter.getDisplayTitle(useReplace = false) TocDomainItem( chapter, - chapter.getDisplayTitle(replaceRules, true), + cachedTitles[chapter.index] ?: baseTitle, downloadState ) } @@ -506,4 +531,56 @@ class TocViewModel( } } } -} \ No newline at end of file + + private fun updateTitleReplaceCacheIfNeeded( + book: Book, + chapters: List, + replaceRules: List, + useReplace: Boolean + ) { + val shouldUseReplace = useReplace && book.getUseReplaceRule() && replaceRules.isNotEmpty() + if (!shouldUseReplace) { + titleCacheJob?.cancel() + titleCacheJob = null + lastTitleCacheKey = null + if (titleReplaceCache.value.isNotEmpty()) { + titleReplaceCache.value = emptyMap() + } + return + } + + val rulesFingerprint = replaceRules.fold(1) { acc, rule -> + var hash = acc + hash = 31 * hash + rule.id.hashCode() + hash = 31 * hash + rule.pattern.hashCode() + hash = 31 * hash + rule.replacement.hashCode() + hash = 31 * hash + rule.isRegex.hashCode() + hash = 31 * hash + rule.timeoutMillisecond.hashCode() + hash + } + + val key = TitleCacheKey( + bookUrl = book.bookUrl, + useReplace = true, + rulesFingerprint = rulesFingerprint, + chineseConverterType = AppConfig.chineseConverterType, + chapterCount = chapters.size + ) + + val isJobActive = titleCacheJob?.isActive == true + if (key == lastTitleCacheKey && (isJobActive || titleReplaceCache.value.isNotEmpty())) { + return + } + + lastTitleCacheKey = key + titleCacheJob?.cancel() + titleReplaceCache.value = emptyMap() + titleCacheJob = viewModelScope.launch(Dispatchers.Default) { + val newCache = HashMap(chapters.size) + chapters.forEach { chapter -> + newCache[chapter.index] = chapter.getDisplayTitle(replaceRules, true) + } + titleReplaceCache.value = newCache + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt index 859988f0a..1990471ea 100644 --- a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt @@ -50,6 +50,7 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard @@ -65,7 +66,6 @@ import io.legado.app.base.BaseRuleEvent import io.legado.app.data.entities.ReplaceRule import io.legado.app.ui.widget.components.ActionItem import io.legado.app.ui.widget.components.DraggableSelectionHandler -import io.legado.app.ui.widget.components.GlassTopAppBarDefaults import io.legado.app.ui.widget.components.GroupManageBottomSheet import io.legado.app.ui.widget.components.card.ReorderableSelectionItem import io.legado.app.ui.widget.components.divider.PillDivider @@ -352,7 +352,7 @@ fun ReplaceRuleScreen( .coerceAtLeast(0), edgePadding = 0.dp, divider = {}, - containerColor = GlassTopAppBarDefaults.containerColor(), + containerColor = Color.Transparent, ) { tabItems.forEachIndexed { index, title -> Tab( diff --git a/app/version.properties b/app/version.properties index 85d146e4d..a5149387f 100644 --- a/app/version.properties +++ b/app/version.properties @@ -1,5 +1,5 @@ VERSION_MAJOR=3 VERSION_MINOR=26 -VERSION_PATCH=8 +VERSION_PATCH=9 VERSION_SUFFIX=0 # 1 = Pre, 0 = Release \ No newline at end of file