Просмотр исходного кода

feat: 阅读界面新增一些设置项入口和目录相关优化

HapeLee 3 месяцев назад
Родитель
Сommit
f40a539724

+ 52 - 5
app/src/main/java/io/legado/app/model/ReadBook.kt

@@ -27,6 +27,10 @@ import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.globalExecutor
+import io.legado.app.model.ReadBook.callBack
+import io.legado.app.model.ReadBook.publishSnapshot
+import io.legado.app.model.ReadBook.renderCallBack
+import io.legado.app.model.ReadBook.snapshot
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.model.localBook.TextFile
 import io.legado.app.model.translation.TranslationChapterState
@@ -41,8 +45,8 @@ import io.legado.app.ui.book.read.page.entities.TextChapter
 import io.legado.app.ui.book.read.page.entities.TextPage
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
-import io.legado.app.ui.book.read.pageestimate.ChapterLengthInfo
 import io.legado.app.ui.book.read.pageestimate.ChapterContentHasher
+import io.legado.app.ui.book.read.pageestimate.ChapterLengthInfo
 import io.legado.app.ui.book.read.pageestimate.LocalPageEstimateCalibrationStore
 import io.legado.app.ui.book.read.pageestimate.LocalPageEstimateMetrics
 import io.legado.app.ui.book.read.pageestimate.PageEstimateConfig
@@ -51,8 +55,8 @@ import io.legado.app.ui.book.read.pageestimate.RoomExactChapterPageCountStore
 import io.legado.app.ui.book.read.pageestimate.WholeBookPageCoordinator
 import io.legado.app.ui.book.read.pageestimate.WholeBookPageState
 import io.legado.app.ui.config.readConfig.ReadConfig
-import io.legado.app.utils.postEvent
 import io.legado.app.utils.dpToPx
+import io.legado.app.utils.postEvent
 import io.legado.app.utils.stackTraceStr
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.CoroutineScope
@@ -69,7 +73,6 @@ import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.takeWhile
 import kotlinx.coroutines.isActive
 import kotlinx.coroutines.launch
@@ -1421,6 +1424,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
         resetPageOffset: Boolean,
         preserveReadAloudPosition: Boolean,
     ) {
+        if (!isCurrentLocalChapter(chapter)) return
         val pageEstimateGeneration = wholeBookPageCoordinator.generation
         val contentProcessor = ContentProcessor.get(book.name, book.origin)
         val displayTitle = chapter.getDisplayTitle(
@@ -1449,6 +1453,10 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
         ).apply {
             this.pageEstimateGeneration = pageEstimateGeneration
         }
+        if (!isCurrentLocalChapter(chapter)) {
+            textChapter.cancelLayout()
+            return
+        }
         when (val offset = chapter.index - durChapterIndex) {
             0 -> curChapterLoadingLock.withLock {
                 publishTextChapter(textChapter) {
@@ -1680,22 +1688,61 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
 
     fun onChapterListUpdated(newBook: Book) {
         if (newBook.isSameNameAuthor(book)) {
+            // 本地 TXT 改目录规则后,旧目录中的章节序号已经不再能表示同一段正文。
+            // 在替换目录前保留当前章节的文件偏移,随后在新目录中重新定位,避免目录
+            // 已更新而阅读页仍显示旧 TextChapter 的标题和序号。
+            val currentChapterStart = curTextChapter?.chapter?.start
             book = newBook
             chapterSize = newBook.totalChapterNum
             simulatedChapterSize = newBook.simulatedTotalChapterNum()
-            if (simulatedChapterSize > 0 && durChapterIndex > simulatedChapterSize - 1) {
+            if (newBook.isLocalTxt && currentChapterStart != null) {
+                val chapters = appDb.bookChapterDao.getChapterList(newBook.bookUrl)
+                val matchedIndex = chapters.indexOfFirst { chapter ->
+                    val start = chapter.start
+                    val end = chapter.end
+                    start != null && end != null && currentChapterStart in start until end
+                }
+                durChapterIndex = if (matchedIndex >= 0) {
+                    matchedIndex
+                } else {
+                    durChapterIndex.coerceIn(0, (simulatedChapterSize - 1).coerceAtLeast(0))
+                }
+                durChapterPos = 0
+                newBook.durChapterIndex = durChapterIndex
+                newBook.durChapterPos = durChapterPos
+            } else if (simulatedChapterSize > 0 && durChapterIndex > simulatedChapterSize - 1) {
                 durChapterIndex = simulatedChapterSize - 1
             }
+            clearTextChapter()
+            synchronized(this) {
+                loadingChapters.clear()
+                downloadedChapters.clear()
+                downloadFailChapters.clear()
+            }
             requestWholeBookPageEstimate()
             publishSnapshot()
             if (callBack == null) {
-                clearTextChapter()
+                return
             } else {
                 loadContent(true)
             }
         }
     }
 
+    /**
+     * TXT 目录重建不会取消尚在读文件的旧内容任务。旧任务回调时章节索引可能仍然
+     * 合法,但标题和偏移已经属于上一版目录,不能再进入排版或覆盖当前正文。
+     */
+    private fun isCurrentLocalChapter(chapter: BookChapter): Boolean {
+        if (book?.isLocalTxt != true) return true
+        val current =
+            appDb.bookChapterDao.getChapter(chapter.bookUrl, chapter.index) ?: return false
+        return current.url == chapter.url &&
+                current.title == chapter.title &&
+                current.start == chapter.start &&
+                current.end == chapter.end
+    }
+
     private fun clearExpiredChapterLoadingJob(clearAll: Boolean = false) {
         if (clearAll) {
             chapterLayoutScheduler.cancelAll()

+ 3 - 1
app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt

@@ -400,13 +400,15 @@ internal val ReadBookButtonIds = listOf(
     "replace",
     "replace_badge",
     "translate",
+    "refresh_current",
 )
 
 internal val MoreActionIds = listOf(
     "change_source", "refresh", "download", "edit_content", "add_bookmark",
     "text_processing", "reverse_content", "re_segment",
     "del_ruby", "del_h", "toc_rule", "charset", "image_style", "page_anim",
-    "simulated_reading", "get_progress", "cover_progress", "log",
+    "simulated_reading", "get_progress", "cover_progress", "highlight_rule",
+    "bottom_button_config", "log",
 )
 
 sealed interface ReadBookIntent {

+ 1 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBarFloating.kt

@@ -401,6 +401,7 @@ private fun loadFloatingIcons(
             }
         },
         "translate" to { onIntent(ReadBookIntent.ToggleTranslation) },
+        "refresh_current" to { onIntent(ReadBookIntent.RefreshCurrentChapter) },
         "ai_summary" to { onIntent(ReadBookIntent.OpenChapterSummary) },
         "ai_rewrite" to { onIntent(ReadBookIntent.OpenAiCurrentChapterRewrite) },
         "more_actions" to { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreActions)) },

+ 3 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBarTool.kt

@@ -503,6 +503,9 @@ internal fun loadToolButtons(
         infoMap.getValue("translate").toButton(isActive = state.translationMode) {
             onIntent(ReadBookIntent.ToggleTranslation)
         },
+        infoMap.getValue("refresh_current").toButton {
+            onIntent(ReadBookIntent.RefreshCurrentChapter)
+        },
         infoMap.getValue("ai_summary").toButton {
             onIntent(ReadBookIntent.OpenChapterSummary)
         },

+ 39 - 23
app/src/main/java/io/legado/app/ui/book/read/sheet/ReaderBookSheet.kt

@@ -39,6 +39,7 @@ import androidx.compose.material.icons.filled.Clear
 import androidx.compose.material.icons.filled.Download
 import androidx.compose.material.icons.filled.ExpandMore
 import androidx.compose.material.icons.filled.LocationOn
+import androidx.compose.material.icons.filled.Lock
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.Numbers
 import androidx.compose.material.icons.filled.Refresh
@@ -75,6 +76,7 @@ import io.legado.app.R
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.help.book.isLocal
+import io.legado.app.help.book.isLocalTxt
 import io.legado.app.ui.book.toc.DownloadState
 import io.legado.app.ui.book.toc.TocActivity
 import io.legado.app.ui.book.toc.TocBookmarkItemUi
@@ -83,7 +85,7 @@ import io.legado.app.ui.book.toc.TocIntent
 import io.legado.app.ui.book.toc.TocItemUi
 import io.legado.app.ui.book.toc.TocUiState
 import io.legado.app.ui.book.toc.TocViewModel
-import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity
+import io.legado.app.ui.book.toc.rule.preview.TxtTocRulePreviewActivity
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.AppFloatingActionButtonMenu
 import io.legado.app.ui.widget.components.EmptyMessage
@@ -192,7 +194,9 @@ fun ReaderBookSheetRoute(
         },
         onEditLocalTocRule = { regex ->
             tocRegexLauncher.launch(
-                Intent(context, TxtTocRuleActivity::class.java).putExtra("tocRegex", regex)
+                Intent(context, TxtTocRulePreviewActivity::class.java)
+                    .putExtra("bookUrl", bookUrl)
+                    .putExtra("tocRegex", regex)
             )
         },
         onExportBookmarks = { isMarkdown, fileName ->
@@ -530,7 +534,6 @@ private fun ReaderBookTocPage(
     val focusRequester = remember { FocusRequester() }
     var menuExpanded by rememberSaveable { mutableStateOf(false) }
     var fabExpanded by rememberSaveable { mutableStateOf(false) }
-    var hasAutoScrolled by rememberSaveable { mutableStateOf(false) }
     val selected = action.selectedIds.isNotEmpty()
     val selectAllText = stringResource(R.string.select_all)
     val invertText = stringResource(R.string.invert_selection)
@@ -570,12 +573,9 @@ private fun ReaderBookTocPage(
     LaunchedEffect(selected) {
         fabExpanded = false
     }
-    LaunchedEffect(action.items) {
-        if (!hasAutoScrolled && action.items.isNotEmpty()) {
-            val currentIndex = action.items.indexOfFirst { it.isDur }
-            if (currentIndex >= 0) listState.scrollToItem(currentIndex)
-            hasAutoScrolled = true
-        }
+    LaunchedEffect(state.book?.totalChapterNum, state.isReverse, action.items.isNotEmpty()) {
+        val currentIndex = action.items.indexOfFirst { it.isDur }
+        if (currentIndex >= 0) listState.scrollToItem(currentIndex)
     }
 
     Column(modifier = Modifier.fillMaxSize()) {
@@ -765,14 +765,28 @@ private fun ReaderSheetChapterItem(
             verticalAlignment = Alignment.CenterVertically,
         ) {
             Column(modifier = Modifier.weight(1f)) {
-                AppText(
+                Row(
                     modifier = Modifier.padding(vertical = 8.dp),
-                    text = item.title,
-                    style = LegadoTheme.typography.labelMediumEmphasized,
-                    color = contentColor,
-                    maxLines = 2,
-                    overflow = TextOverflow.Ellipsis,
-                )
+                    verticalAlignment = Alignment.CenterVertically,
+                ) {
+                    if (item.isVip && !item.isPay) {
+                        Icon(
+                            imageVector = Icons.Default.Lock,
+                            contentDescription = stringResource(R.string.a11y_vip_locked),
+                            tint = LegadoTheme.colorScheme.error,
+                            modifier = Modifier
+                                .size(14.dp)
+                                .padding(end = 4.dp),
+                        )
+                    }
+                    AppText(
+                        text = item.title,
+                        style = LegadoTheme.typography.labelMediumEmphasized,
+                        color = contentColor,
+                        maxLines = 2,
+                        overflow = TextOverflow.Ellipsis,
+                    )
+                }
                 if (!item.tag.isNullOrBlank()) {
                     AppText(
                         text = item.tag,
@@ -905,13 +919,15 @@ private fun ReaderBookTocMenu(
         onClick = { dispatch(TocIntent.UpdateToc) },
     )
     if (book?.isLocal == true) {
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.local_book_toc_rule),
-            onClick = {
-                onDismiss()
-                onEditLocalTocRule(book.tocUrl)
-            },
-        )
+        if (book.isLocalTxt) {
+            RoundDropdownMenuItem(
+                text = stringResource(R.string.txt_toc_rule),
+                onClick = {
+                    onDismiss()
+                    onEditLocalTocRule(book.tocUrl)
+                },
+            )
+        }
         RoundDropdownMenuItem(
             text = stringResource(R.string.split_long_chapters),
             isSelected = state.isSplitLongChapter,

+ 7 - 0
app/src/main/java/io/legado/app/ui/book/read/sheet/ReaderMoreActionsSheet.kt

@@ -34,6 +34,7 @@ import androidx.compose.material.icons.filled.SwapVert
 import androidx.compose.material.icons.filled.Sync
 import androidx.compose.material.icons.filled.Toc
 import androidx.compose.material.icons.filled.Translate
+import androidx.compose.material.icons.filled.Tune
 import androidx.compose.material3.HorizontalDivider
 import androidx.compose.material3.Icon
 import androidx.compose.runtime.Composable
@@ -380,6 +381,9 @@ private fun moreActionSpecs(
     MoreActionSpec(
         "text_processing", stringResource(R.string.text_processing), Icons.Default.FindReplace,
         onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing)) }),
+    MoreActionSpec(
+        "highlight_rule", stringResource(R.string.highlight_rule_config), Icons.Default.Tune,
+        onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.HighlightRuleConfig)) }),
     MoreActionSpec(
         "reverse_content", stringResource(R.string.reverse_content), Icons.Default.SwapVert,
         onClick = { dispatch(ReadBookIntent.MenuReverseContent) }),
@@ -411,6 +415,9 @@ private fun moreActionSpecs(
         "cover_progress", stringResource(R.string.cover_book_progress), Icons.Default.Sync,
         applicable = state.isReadingProgressSyncConfigured,
         onClick = { dispatch(ReadBookIntent.MenuCoverProgress) }),
+    MoreActionSpec(
+        "bottom_button_config", stringResource(R.string.config_btn), Icons.Default.Settings,
+        onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ToolButtonConfig)) }),
     MoreActionSpec(
         "log", stringResource(R.string.log), Icons.Default.BugReport,
         onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.AppLog)) }),

+ 6 - 0
app/src/main/java/io/legado/app/ui/book/read/sheet/SystemMenuPage.kt

@@ -27,6 +27,7 @@ import androidx.compose.material.icons.filled.FindReplace
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.PlayArrow
 import androidx.compose.material.icons.filled.RecordVoiceOver
+import androidx.compose.material.icons.filled.Refresh
 import androidx.compose.material.icons.filled.Search
 import androidx.compose.material.icons.filled.Settings
 import androidx.compose.material.icons.filled.SkipNext
@@ -904,6 +905,11 @@ internal fun readMenuButtonInfos(context: Context): List<ReadMenuButtonInfo> = l
         context.getString(R.string.text_processing)
     ),
     ReadMenuButtonInfo("translate", Icons.Default.Translate, context.getString(R.string.translate)),
+    ReadMenuButtonInfo(
+        "refresh_current",
+        Icons.Default.Refresh,
+        context.getString(R.string.menu_refresh_dur)
+    ),
     ReadMenuButtonInfo(
         "ai_summary",
         Icons.Default.AutoAwesome,

+ 32 - 0
app/src/main/java/io/legado/app/ui/book/read/sheet/TypographyTabs.kt

@@ -435,6 +435,8 @@ internal fun TypographyTitleTab(
     var titleSegScaling by remember(config.titleSegScaling) { mutableFloatStateOf(config.titleSegScaling) }
     var titleLineSpacingExtra by remember(config.titleLineSpacingExtra) { mutableIntStateOf(config.titleLineSpacingExtra) }
     var titleLineSpacingSub by remember(config.titleLineSpacingSub) { mutableIntStateOf(config.titleLineSpacingSub) }
+    var titleTopSpacing by remember(config.titleTopSpacing) { mutableIntStateOf(config.titleTopSpacing) }
+    var titleBottomSpacing by remember(config.titleBottomSpacing) { mutableIntStateOf(config.titleBottomSpacing) }
 
     var titleSize by remember(config.titleSize) {
         mutableIntStateOf(config.titleSize)
@@ -588,6 +590,36 @@ internal fun TypographyTitleTab(
             },
         )
 
+        SectionTitle(title = stringResource(R.string.title_padding))
+        TinySliderSettingItem(
+            title = stringResource(R.string.title_margin_top),
+            value = titleTopSpacing.toFloat(),
+            valueRange = 0f..200f,
+            imageVector = Icons.Default.VerticalAlignTop,
+            onValueChange = { value ->
+                titleTopSpacing = value.toInt()
+                onIntent(
+                    ReadBookIntent.UpdateConfig(
+                        ConfigUpdate.TitleTopSpacing(titleTopSpacing)
+                    )
+                )
+            },
+        )
+        TinySliderSettingItem(
+            title = stringResource(R.string.title_margin_bottom),
+            value = titleBottomSpacing.toFloat(),
+            valueRange = 0f..200f,
+            imageVector = Icons.Default.VerticalAlignBottom,
+            onValueChange = { value ->
+                titleBottomSpacing = value.toInt()
+                onIntent(
+                    ReadBookIntent.UpdateConfig(
+                        ConfigUpdate.TitleBottomSpacing(titleBottomSpacing)
+                    )
+                )
+            },
+        )
+
         Spacer(Modifier.height(8.dp))
     }
 

+ 1 - 0
app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt

@@ -581,6 +581,7 @@ class TocViewModel(
             kotlin.runCatching {
                 LocalBook.getChapterList(book).let {
                     bookRepository.replaceChaptersAndUpdateBook(book, it)
+                    ReadBook.onChapterListUpdated(book)
                 }
             }.onFailure {
                 AppLog.put("LoadTocError:${it.localizedMessage}", it)

+ 1 - 2
app/src/main/java/io/legado/app/ui/book/toc/rule/preview/TxtTocRulePreviewContract.kt

@@ -3,8 +3,8 @@ package io.legado.app.ui.book.toc.rule.preview
 import androidx.compose.runtime.Stable
 import io.legado.app.data.entities.TxtTocRule
 import kotlinx.collections.immutable.ImmutableList
-import kotlinx.collections.immutable.toImmutableList
 import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
 
 @Stable
 data class TxtTocRulePreviewUiState(
@@ -18,7 +18,6 @@ data class TxtTocRulePreviewUiState(
     val searchQuery: String = "",
     val showSearch: Boolean = false,
 ) {
-    val hasSelection: Boolean get() = selectedRule.isNotEmpty()
     val filteredRules: ImmutableList<TocRulePreviewItem>
         get() = if (searchQuery.isBlank()) rules
         else rules.filter {

+ 35 - 67
app/src/main/java/io/legado/app/ui/book/toc/rule/preview/TxtTocRulePreviewScreen.kt

@@ -1,11 +1,11 @@
 package io.legado.app.ui.book.toc.rule.preview
 
-import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.expandVertically
 import androidx.compose.animation.fadeIn
-import androidx.compose.animation.shrinkVertically
 import androidx.compose.animation.fadeOut
+import androidx.compose.animation.shrinkVertically
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
@@ -27,7 +27,6 @@ import androidx.compose.foundation.lazy.itemsIndexed
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.outlined.FormatListBulleted
-import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.GridView
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.ExperimentalMaterial3Api
@@ -48,7 +47,6 @@ import androidx.compose.ui.unit.dp
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.data.entities.TxtTocRule
-import io.legado.app.utils.toastOnUi
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
@@ -56,17 +54,17 @@ import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.SearchBar
 import io.legado.app.ui.widget.components.card.GlassCard
 import io.legado.app.ui.widget.components.icon.AppIcons
-import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
-import io.legado.app.ui.widget.components.topbar.TopBarActionButton
-import io.legado.app.ui.widget.components.AppFloatingActionButton
-import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
-import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
-import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
+import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
 import io.legado.app.ui.widget.components.rules.RuleEditFields
 import io.legado.app.ui.widget.components.rules.RuleEditSheet
 import io.legado.app.ui.widget.components.text.AppText
+import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
+import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
+import io.legado.app.ui.widget.components.topbar.TopBarActionButton
+import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
+import io.legado.app.utils.toastOnUi
 import org.koin.androidx.compose.koinViewModel
 
 @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -113,16 +111,27 @@ fun TxtTocRulePreviewScreen(
     // Chapter list bottom sheet
     when (val sheet = state.activeSheet) {
         is TxtTocRulePreviewSheet.ChapterList -> {
+            val item = state.rules.firstOrNull { it.rule.id == sheet.item.rule.id } ?: sheet.item
             AppModalBottomSheet(
-                data = sheet.item,
+                data = item,
                 onDismissRequest = { onIntent(TxtTocRulePreviewIntent.DismissSheet) },
+                title = item.rule.name,
+                startAction = {
+                    TopBarActionButton(
+                        onClick = { onIntent(TxtTocRulePreviewIntent.EditRule(item.rule)) },
+                        imageVector = AppIcons.Edit,
+                        contentDescription = stringResource(R.string.edit),
+                    )
+                },
+                endAction = {
+                    TopBarActionButton(
+                        onClick = { onIntent(TxtTocRulePreviewIntent.ApplyRule) },
+                        imageVector = AppIcons.Check,
+                        contentDescription = stringResource(R.string.ok),
+                    )
+                },
             ) {
-                ChapterListSheetContent(
-                    item = sheet.item,
-                    onEditRule = { rule ->
-                        onIntent(TxtTocRulePreviewIntent.EditRule(rule))
-                    },
-                )
+                ChapterListSheetContent(item = item)
             }
         }
 
@@ -221,15 +230,6 @@ fun TxtTocRulePreviewScreen(
                 },
             )
         },
-        floatingActionButton = {
-            if (state.hasSelection) {
-                AppFloatingActionButton(
-                    onClick = { onIntent(TxtTocRulePreviewIntent.ApplyRule) },
-                    icon = Icons.Default.Check,
-                    tooltipText = stringResource(R.string.ok),
-                )
-            }
-        },
     ) { contentPadding ->
         if (state.loading) {
             Box(
@@ -247,7 +247,7 @@ fun TxtTocRulePreviewScreen(
                     modifier = Modifier.fillMaxSize(),
                     contentPadding = adaptiveContentPadding(
                         top = contentPadding.calculateTopPadding(),
-                        bottom = contentPadding.calculateBottomPadding() + 80.dp,
+                        bottom = contentPadding.calculateBottomPadding(),
                     ),
                     horizontalArrangement = Arrangement.spacedBy(8.dp),
                     verticalArrangement = Arrangement.spacedBy(8.dp),
@@ -258,9 +258,7 @@ fun TxtTocRulePreviewScreen(
                             isSelected = item.rule.chapterRule == state.selectedRule,
                             onClick = {
                                 onIntent(TxtTocRulePreviewIntent.SelectRule(item.rule.chapterRule))
-                                if (item.totalCount > 0) {
-                                    onIntent(TxtTocRulePreviewIntent.ShowChapterList(item))
-                                }
+                                onIntent(TxtTocRulePreviewIntent.ShowChapterList(item))
                             },
                         )
                     }
@@ -271,7 +269,7 @@ fun TxtTocRulePreviewScreen(
                     modifier = Modifier.fillMaxSize(),
                     contentPadding = adaptiveContentPadding(
                         top = contentPadding.calculateTopPadding(),
-                        bottom = contentPadding.calculateBottomPadding() + 80.dp,
+                        bottom = contentPadding.calculateBottomPadding(),
                     ),
                     verticalArrangement = Arrangement.spacedBy(4.dp),
                 ) {
@@ -281,9 +279,7 @@ fun TxtTocRulePreviewScreen(
                             isSelected = item.rule.chapterRule == state.selectedRule,
                             onClick = {
                                 onIntent(TxtTocRulePreviewIntent.SelectRule(item.rule.chapterRule))
-                                if (item.totalCount > 0) {
-                                    onIntent(TxtTocRulePreviewIntent.ShowChapterList(item))
-                                }
+                                onIntent(TxtTocRulePreviewIntent.ShowChapterList(item))
                             },
                         )
                     }
@@ -441,45 +437,17 @@ private fun RulePreviewListItem(
 @Composable
 private fun ChapterListSheetContent(
     item: TocRulePreviewItem,
-    onEditRule: (TxtTocRule) -> Unit,
 ) {
     Column(
         modifier = Modifier
             .fillMaxWidth()
             .padding(16.dp),
     ) {
-        // Header
-        Row(
-            modifier = Modifier.fillMaxWidth(),
-            verticalAlignment = Alignment.CenterVertically,
-        ) {
-            Text(
-                text = item.rule.name,
-                style = LegadoTheme.typography.titleMedium,
-                modifier = Modifier.weight(1f),
-                maxLines = 1,
-                overflow = TextOverflow.Ellipsis,
-            )
-
-            Spacer(modifier = Modifier.width(8.dp))
-
-            TopBarActionButton(
-                onClick = { onEditRule(item.rule) },
-                imageVector = AppIcons.Edit,
-                contentDescription = stringResource(R.string.edit),
-            )
-
-            Spacer(modifier = Modifier.width(8.dp))
-
-            Text(
-                text = stringResource(
-                    R.string.chapter_count_format,
-                    item.totalCount
-                ),
-                style = LegadoTheme.typography.bodyMedium,
-                color = LegadoTheme.colorScheme.onSurfaceVariant,
-            )
-        }
+        Text(
+            text = stringResource(R.string.chapter_count_format, item.totalCount),
+            style = LegadoTheme.typography.bodyMedium,
+            color = LegadoTheme.colorScheme.onSurfaceVariant,
+        )
 
         Spacer(modifier = Modifier.height(12.dp))
 

+ 9 - 3
app/src/main/java/io/legado/app/ui/book/toc/rule/preview/TxtTocRulePreviewViewModel.kt

@@ -3,6 +3,7 @@ package io.legado.app.ui.book.toc.rule.preview
 import android.app.Application
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
+import io.legado.app.R
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.TxtTocRule
 import io.legado.app.data.repository.BookRepository
@@ -10,20 +11,19 @@ import io.legado.app.data.repository.TxtTocRuleRepository
 import io.legado.app.help.DefaultData
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.utils.Utf8BomUtils
-import io.legado.app.R
 import kotlinx.collections.immutable.toImmutableList
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.update
-import kotlinx.coroutines.ensureActive
-import kotlin.coroutines.coroutineContext
 import kotlinx.coroutines.launch
 import java.util.regex.Pattern
 import java.util.regex.PatternSyntaxException
+import kotlin.coroutines.coroutineContext
 
 class TxtTocRulePreviewViewModel(
     private val app: Application,
@@ -197,6 +197,12 @@ class TxtTocRulePreviewViewModel(
             _uiState.update {
                 it.copy(
                     rules = currentRules.toImmutableList(),
+                    selectedRule = if (it.selectedRule == existing?.chapterRule) {
+                        updatedRule.chapterRule
+                    } else {
+                        it.selectedRule
+                    },
+                    activeSheet = TxtTocRulePreviewSheet.ChapterList(refreshed),
                     editingRule = null,
                 )
             }

+ 42 - 115
app/src/main/java/io/legado/app/ui/widget/components/button/series/MediumToggleButton.kt

@@ -2,29 +2,14 @@ package io.legado.app.ui.widget.components.button.series
 
 import androidx.compose.animation.animateColorAsState
 import androidx.compose.animation.core.tween
-import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
-import androidx.compose.material3.Icon
-import androidx.compose.material3.Text
-import androidx.compose.material3.ToggleButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.unit.dp
 import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.theme.LegadoTheme.composeEngine
-import io.legado.app.ui.theme.ThemeResolver
-import top.yukonga.miuix.kmp.theme.MiuixTheme
-import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
-import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
-import top.yukonga.miuix.kmp.basic.Text as MiuixText
 
-@OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
 fun MediumToggleButton(
     checked: Boolean,
@@ -38,107 +23,49 @@ fun MediumToggleButton(
     text: String? = null,
     contentDescription: String? = null
 ) {
-    if (ThemeResolver.isMiuixEngine(composeEngine)) {
-        val containerColor by animateColorAsState(
-            targetValue = if (checked) MiuixTheme.colorScheme.primaryContainer else MiuixTheme.colorScheme.surfaceContainerHigh,
-            animationSpec = tween(150),
-            label = "MiuixToggleContainerColor"
-        )
-
-        val contentColor by animateColorAsState(
-            targetValue = if (checked) MiuixTheme.colorScheme.onPrimaryContainer else MiuixTheme.colorScheme.onSurface,
-            animationSpec = tween(150),
-            label = "MiuixToggleContentColor"
-        )
-
-        if (text != null) {
-            MiuixIconButton(
-                onClick = { onCheckedChange(!checked) },
-                modifier = modifier,
-                enabled = enabled,
-                backgroundColor = containerColor
-            ) {
-                Row(
-                    verticalAlignment = Alignment.CenterVertically,
-                    horizontalArrangement = Arrangement.Center,
-                    modifier = Modifier.padding(horizontal = 8.dp)
-                ) {
-                    MiuixIcon(
-                        imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
-                        contentDescription = null,
-                        tint = contentColor
-                    )
-                    MiuixText(
-                        text = text,
-                        color = contentColor,
-                        style = LegadoTheme.typography.labelMedium,
-                        modifier = Modifier.padding(start = 8.dp),
-                        maxLines = 1,
-                        softWrap = false
-                    )
-                }
-            }
+    val containerColor by animateColorAsState(
+        targetValue = if (checked) {
+            LegadoTheme.colorScheme.primaryContainer
         } else {
-            SeriesIconButton(
-                icon = if (checked) (iconChecked ?: icon)!! else icon!!,
-                contentDescription = contentDescription,
-                onClick = { onCheckedChange(!checked) },
-                modifier = modifier,
-                enabled = enabled,
-                selected = checked,
-                onLongClick = onLongClick,
-                size = MediumSeriesIconButtonSize,
-                iconSize = MediumSeriesIconSize,
-                style = when (style) {
-                    ToggleStyle.Outlined -> SeriesIconButtonStyle.Outlined
-                    ToggleStyle.Tonal -> SeriesIconButtonStyle.Tonal
-                },
-                selectedContainerColor = containerColor,
-                selectedContentColor = contentColor
-            )
-        }
-    } else {
-        if (text != null) {
-            ToggleButton(
-                checked = checked,
-                onCheckedChange = onCheckedChange,
-                modifier = modifier,
-                enabled = enabled,
-                contentPadding = PaddingValues(horizontal = 8.dp)
-            ) {
-                Row(
-                    verticalAlignment = Alignment.CenterVertically,
-                    horizontalArrangement = Arrangement.Center
-                ) {
-                    Icon(
-                        imageVector = if (checked) (iconChecked ?: icon)!! else icon!!,
-                        contentDescription = null
-                    )
-                    Text(
-                        text = text,
-                        style = LegadoTheme.typography.labelMedium,
-                        modifier = Modifier.padding(start = 8.dp),
-                        maxLines = 1,
-                        softWrap = false
-                    )
-                }
-            }
+            LegadoTheme.colorScheme.surfaceContainer
+        },
+        animationSpec = tween(150),
+        label = "MediumToggleContainerColor"
+    )
+    val contentColor by animateColorAsState(
+        targetValue = if (checked) {
+            LegadoTheme.colorScheme.onPrimaryContainer
         } else {
-            SeriesIconButton(
-                icon = if (checked) (iconChecked ?: icon)!! else icon!!,
-                contentDescription = contentDescription,
-                onClick = { onCheckedChange(!checked) },
-                modifier = modifier,
-                enabled = enabled,
-                selected = checked,
-                onLongClick = onLongClick,
-                size = MediumSeriesIconButtonSize,
-                iconSize = MediumSeriesIconSize,
-                style = when (style) {
-                    ToggleStyle.Outlined -> SeriesIconButtonStyle.Outlined
-                    ToggleStyle.Tonal -> SeriesIconButtonStyle.Tonal
-                }
-            )
-        }
+            LegadoTheme.colorScheme.onSurfaceVariant
+        },
+        animationSpec = tween(150),
+        label = "MediumToggleContentColor"
+    )
+    SeriesButton(
+        onClick = { onCheckedChange(!checked) },
+        modifier = modifier,
+        enabled = enabled,
+        selected = checked,
+        onLongClick = onLongClick,
+        size = if (text == null) MediumSeriesIconButtonSize else null,
+        style = when (style) {
+            ToggleStyle.Outlined -> SeriesIconButtonStyle.Outlined
+            ToggleStyle.Tonal -> SeriesIconButtonStyle.Tonal
+        },
+        containerColor = containerColor,
+        selectedContainerColor = containerColor,
+        contentColor = contentColor,
+        selectedContentColor = contentColor
+    ) { resolvedContentColor ->
+        SeriesButtonContent(
+            icon = if (checked) (iconChecked ?: icon)!! else icon!!,
+            text = text,
+            contentDescription = contentDescription,
+            iconSize = MediumSeriesIconSize,
+            textStyle = LegadoTheme.typography.labelMedium,
+            contentColor = resolvedContentColor,
+            padding = PaddingValues(horizontal = 8.dp, vertical = 6.dp),
+            spacing = 8.dp
+        )
     }
 }