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

refactor(read): 重构排版配置页面,整合页眉页脚设置

feat: 新增页脚字体、字号配置与独立样式开关
feat: 目录页新增更新目录功能
feat: 替换规则页面支持书籍上下文功能
fix: 替换规则跳转未传递书籍参数
style: 优化字符串资源与页面布局命名
refactor: 合并原页眉页脚、文字排版等配置到排版配置页
refactor: 移除冗余的边距配置弹窗,整合到全局设置
52fisher 3 месяцев назад
Родитель
Сommit
943b29b730
26 измененных файлов с 2024 добавлено и 1944 удалено
  1. 3 0
      app/src/main/java/io/legado/app/data/repository/ReadBookStyleConfigRepository.kt
  2. 3 0
      app/src/main/java/io/legado/app/domain/gateway/ReadStyleMutation.kt
  3. 9 0
      app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt
  4. 23 3
      app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt
  5. 59 162
      app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt
  6. 6 1
      app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt
  7. 2 2
      app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt
  8. 18 0
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  9. 12 0
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  10. 131 23
      app/src/main/java/io/legado/app/ui/book/read/sheet/GlobalThemePage.kt
  11. 0 699
      app/src/main/java/io/legado/app/ui/book/read/sheet/HeaderFooterPage.kt
  12. 0 40
      app/src/main/java/io/legado/app/ui/book/read/sheet/MoreConfigSheet.kt
  13. 0 250
      app/src/main/java/io/legado/app/ui/book/read/sheet/PaddingConfigSheet.kt
  14. 8 7
      app/src/main/java/io/legado/app/ui/book/read/sheet/ReadStyleSheet.kt
  15. 0 734
      app/src/main/java/io/legado/app/ui/book/read/sheet/TextTitleSheet.kt
  16. 313 0
      app/src/main/java/io/legado/app/ui/book/read/sheet/TypographyPage.kt
  17. 1066 0
      app/src/main/java/io/legado/app/ui/book/read/sheet/TypographyTabs.kt
  18. 7 0
      app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt
  19. 50 0
      app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt
  20. 6 1
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt
  21. 33 2
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleContract.kt
  22. 72 0
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt
  23. 178 1
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt
  24. 3 3
      app/src/main/res/layout/dialog_read_info.xml
  25. 11 8
      app/src/main/res/values-zh-rCN/strings.xml
  26. 11 8
      app/src/main/res/values/strings.xml

+ 3 - 0
app/src/main/java/io/legado/app/data/repository/ReadBookStyleConfigRepository.kt

@@ -180,6 +180,7 @@ class ReadBookStyleConfigRepository(
             ReadStyleIntKey.TipFooterMiddle -> ReadBookConfig.tipFooterMiddle = value
             ReadStyleIntKey.TipFooterRight -> ReadBookConfig.tipFooterRight = value
             ReadStyleIntKey.HeaderFontSize -> ReadBookConfig.headerFontSize = value
+            ReadStyleIntKey.FooterFontSize -> ReadBookConfig.footerFontSize = value
             ReadStyleIntKey.PageAnim -> ReadBookConfig.pageAnim = value
             ReadStyleIntKey.UnderlineHeight -> ReadBookConfig.underlineHeight = value
             ReadStyleIntKey.UnderlinePadding -> ReadBookConfig.underlinePadding = value
@@ -223,6 +224,7 @@ class ReadBookStyleConfigRepository(
             ReadStyleBooleanKey.UnderlineExtend -> ReadBookConfig.underlineExtend = value
             ReadStyleBooleanKey.ShowHeaderLine -> ReadBookConfig.showHeaderLine = value
             ReadStyleBooleanKey.ShowFooterLine -> ReadBookConfig.showFooterLine = value
+            ReadStyleBooleanKey.ApplyHeaderStyle -> ReadBookConfig.applyHeaderStyle = value
             ReadStyleBooleanKey.StatusIconDark -> ReadBookConfig.durConfig.setCurStatusIconDark(value)
         }
     }
@@ -234,6 +236,7 @@ class ReadBookStyleConfigRepository(
             ReadStyleStringKey.TitleFont -> ReadBookConfig.titleFont = value
             ReadStyleStringKey.TitleSegFlag -> ReadBookConfig.titleSegFlag = value
             ReadStyleStringKey.HeaderFont -> ReadBookConfig.headerFont = value
+            ReadStyleStringKey.FooterFont -> ReadBookConfig.footerFont = value
             ReadStyleStringKey.CustomTipHeaderLeft -> ReadBookConfig.customTipHeaderLeft = value
             ReadStyleStringKey.CustomTipHeaderMiddle -> ReadBookConfig.customTipHeaderMiddle = value
             ReadStyleStringKey.CustomTipHeaderRight -> ReadBookConfig.customTipHeaderRight = value

+ 3 - 0
app/src/main/java/io/legado/app/domain/gateway/ReadStyleMutation.kt

@@ -32,6 +32,7 @@ enum class ReadStyleIntKey {
     TipFooterMiddle,
     TipFooterRight,
     HeaderFontSize,
+    FooterFontSize,
     PageAnim,
     UnderlineHeight,
     UnderlinePadding,
@@ -71,6 +72,7 @@ enum class ReadStyleBooleanKey {
     UnderlineExtend,
     ShowHeaderLine,
     ShowFooterLine,
+    ApplyHeaderStyle,
     StatusIconDark,
 }
 
@@ -80,6 +82,7 @@ enum class ReadStyleStringKey {
     TitleFont,
     TitleSegFlag,
     HeaderFont,
+    FooterFont,
     CustomTipHeaderLeft,
     CustomTipHeaderMiddle,
     CustomTipHeaderRight,

+ 9 - 0
app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt

@@ -357,6 +357,12 @@ object ReadBookConfig {
             config.footerFontSize = value
         }
 
+    var applyHeaderStyle: Boolean
+        get() = config.applyHeaderStyle
+        set(value) {
+            config.applyHeaderStyle = value
+        }
+
     var textBold: Int
         get() = config.textBold
         set(value) {
@@ -815,6 +821,7 @@ object ReadBookConfig {
             exportConfig.footerFont = shareConfig.footerFont
             exportConfig.headerFontSize = shareConfig.headerFontSize
             exportConfig.footerFontSize = shareConfig.footerFontSize
+            exportConfig.applyHeaderStyle = shareConfig.applyHeaderStyle
             exportConfig.textBold = shareConfig.textBold
             exportConfig.textSize = shareConfig.textSize
             exportConfig.letterSpacing = shareConfig.letterSpacing
@@ -924,6 +931,7 @@ object ReadBookConfig {
         var footerFont: String = "",//页脚字体
         var headerFontSize: Int = 12,//页眉字号
         var footerFontSize: Int = 12,//页脚字号
+        var applyHeaderStyle: Boolean = true,//页脚是否应用页眉字体样式
         var textBold: Int = 500,//是否粗体字 0:正常, 1:粗体, 2:细体
         var textSize: Int = 20,//文字大小
         var textItalic: Boolean = false,// 是否启用斜体
@@ -1089,6 +1097,7 @@ object ReadBookConfig {
             "footerFont" to footerFont,
             "headerFontSize" to headerFontSize,
             "footerFontSize" to footerFontSize,
+            "applyHeaderStyle" to applyHeaderStyle,
             "textBold" to textBold,
             "textSize" to textSize,
             "letterSpacing" to letterSpacing,

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

@@ -58,11 +58,9 @@ data class ReadBookMenuState(
 sealed interface ReadBookMenuRoute {
     data object Main : ReadBookMenuRoute
     data object ReadStyle : ReadBookMenuRoute
-    data object TextTitle : ReadBookMenuRoute
     data object ReadAloud : ReadBookMenuRoute
     data object AutoRead : ReadBookMenuRoute
-    data object PaddingConfig : ReadBookMenuRoute
-    data object HeaderFooterConfig : ReadBookMenuRoute
+    data object TypographyConfig : ReadBookMenuRoute
 }
 
 @Stable
@@ -154,6 +152,19 @@ data class ReadSheetConfigUiState(
     val footerPaddingBottom: Int = 0,
     val footerPaddingLeft: Int = 0,
     val footerPaddingRight: Int = 0,
+    // 页眉页脚字体
+    val headerFont: String = "",
+    val footerFont: String = "",
+    val headerFontSize: Int = 12,
+    val footerFontSize: Int = 12,
+    val applyHeaderStyle: Boolean = true,
+    val tipDividerColor: Int = 0,
+    val tipHeaderColor: Int = 0,
+    val tipHeaderColorNight: Int = 0,
+    val tipFooterColor: Int = 0,
+    val tipFooterColorNight: Int = 0,
+    val textFullJustify: Boolean = true,
+    val textBottomJustify: Boolean = true,
     val configNames: ImmutableList<String> = persistentListOf(),
 )
 
@@ -1194,6 +1205,15 @@ sealed interface ConfigUpdate {
     data class HeaderFontSize(val value: Int) : ConfigUpdate {
         override val actions = setOf(ConfigUpdateAction.UpdateStyle)
     }
+    data class FooterFont(val path: String) : ConfigUpdate {
+        override val actions = setOf(ConfigUpdateAction.UpdateStyle)
+    }
+    data class FooterFontSize(val value: Int) : ConfigUpdate {
+        override val actions = setOf(ConfigUpdateAction.UpdateStyle)
+    }
+    data class ApplyHeaderStyle(val value: Boolean) : ConfigUpdate {
+        override val actions = setOf(ConfigUpdateAction.UpdateStyle)
+    }
     data class TipHeaderColor(val color: Int) : ConfigUpdate {
         override val actions = setOf(ConfigUpdateAction.UpdateStyle)
     }

+ 59 - 162
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt

@@ -81,6 +81,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.Icon
 import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
@@ -158,12 +159,10 @@ import io.legado.app.data.repository.ReadPreferences
 import io.legado.app.help.config.ReadStyleResolver
 import io.legado.app.ui.animation.DampedDragAnimation
 import io.legado.app.ui.book.read.sheet.AutoReadContent
-import io.legado.app.ui.book.read.sheet.HeaderFooterPage
-import io.legado.app.ui.book.read.sheet.PaddingConfigContent
 import io.legado.app.ui.book.read.sheet.ReadAloudContent
 import io.legado.app.ui.book.read.sheet.ReadMenuButtonInfo
 import io.legado.app.ui.book.read.sheet.ReadStyleContent
-import io.legado.app.ui.book.read.sheet.ReadStyleTextTitleContent
+import io.legado.app.ui.book.read.sheet.TypographyPage
 import io.legado.app.ui.book.read.sheet.readMenuButtonInfos
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.AppSlider
@@ -210,15 +209,14 @@ fun ReadBookMenuBar(
     } else {
         ReadBookMenuContent.Route(currentRoute)
     }
-    val dialogLikeRoute = currentRoute == ReadBookMenuRoute.PaddingConfig || currentRoute == ReadBookMenuRoute.HeaderFooterConfig
+    val dialogLikeRoute = currentRoute == ReadBookMenuRoute.TypographyConfig
     var readStylePage by remember { mutableIntStateOf(0) }
     LaunchedEffect(currentRoute) {
         if (currentRoute != ReadBookMenuRoute.ReadStyle) {
             readStylePage = 0
         }
     }
-    val hideTopBar = dialogLikeRoute ||
-            currentRoute == ReadBookMenuRoute.TextTitle
+    val hideTopBar = dialogLikeRoute
     val menuColors = readMenuColors(preferences.readBarStyle)
 
     Box(Modifier.fillMaxSize()) {
@@ -436,7 +434,7 @@ private fun ReadBookMenuSurface(
         is ReadBookMenuContent.Route -> contentTarget.route
     }
     val expanded = route != ReadBookMenuRoute.Main
-    val dialogLikeRoute = route == ReadBookMenuRoute.PaddingConfig || route == ReadBookMenuRoute.HeaderFooterConfig
+    val dialogLikeRoute = route == ReadBookMenuRoute.TypographyConfig
     val density = LocalDensity.current
     val windowSize = LocalWindowInfo.current.containerSize
     var surfaceHeightPx by remember { mutableIntStateOf(0) }
@@ -700,11 +698,8 @@ private fun ReadBookMenuSurface(
                             onBack = { onIntent(ReadBookIntent.ReadMenuBack) },
                         ) {
                             ReadStyleContent(
-                                onOpenPaddingConfig = {
-                                    onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.PaddingConfig))
-                                },
-                                onOpenHeaderFooterConfig = {
-                                    onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.HeaderFooterConfig))
+                                onOpenTypographyConfig = {
+                                    onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.TypographyConfig))
                                 },
                                 onOpenMoreConfig = {
                                     onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreConfig))
@@ -712,8 +707,11 @@ private fun ReadBookMenuSurface(
                                 onOpenBgTextConfig = { index ->
                                     onIntent(ReadBookIntent.OpenBgTextConfig(index))
                                 },
-                                onOpenTextTitle = {
-                                    onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.TextTitle))
+                                onOpenUnderlineConfig = {
+                                    onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.UnderlineConfig))
+                                },
+                                onOpenShadowSet = {
+                                    onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ShadowSet))
                                 },
                                 onOpenFontSelect = {
                                     onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.FontSelect))
@@ -732,48 +730,24 @@ private fun ReadBookMenuSurface(
                         }
                     }
 
-                    ReadBookMenuRoute.PaddingConfig -> {
+                    ReadBookMenuRoute.TypographyConfig -> {
                         ReadBookMenuRoutePage(
-                            title = stringResource(R.string.padding),
+                            title = stringResource(R.string.compose_type),
                             maxHeight = maxHeight,
                             scrollContent = false,
                             animateSize = false,
                             bottomPadding = if (extendSurfaceToNavigationBar) navBarHeight else 0.dp,
                             onBack = { onIntent(ReadBookIntent.ReadMenuBack) },
                         ) {
-                            PaddingConfigContent(
+                            TypographyPage(
                                 config = state.sheetConfig,
                                 onIntent = onIntent,
-                                modifier = Modifier.padding(horizontal = 16.dp),
-                            )
-                        }
-                    }
-
-                    ReadBookMenuRoute.HeaderFooterConfig -> {
-                        ReadBookMenuRoutePage(
-                            title = stringResource(R.string.header_footer),
-                            maxHeight = maxHeight,
-                            scrollContent = false,
-                            animateSize = false,
-                            bottomPadding = if (extendSurfaceToNavigationBar) navBarHeight else 0.dp,
-                            onBack = { onIntent(ReadBookIntent.ReadMenuBack) },
-                        ) {
-                            HeaderFooterPage(
-                                onIntent = onIntent,
-                            )
-                        }
-                    }
-
-                    ReadBookMenuRoute.TextTitle -> {
-                        ReadBookMenuRoutePage(
-                            title = stringResource(R.string.read_config_text_effects),
-                            maxHeight = maxHeight,
-                            bottomPadding = if (extendSurfaceToNavigationBar) navBarHeight else 0.dp,
-                            animateSize = false,
-                            onBack = { onIntent(ReadBookIntent.ReadMenuBack) },
-                        ) {
-                            ReadStyleTextTitleContent(
-                                config = state.sheetConfig,
+                                onOpenFontSelect = {
+                                    onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.FontSelect))
+                                },
+                                onOpenTitleFontSelect = {
+                                    onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TitleFontSelect))
+                                },
                                 onOpenShadowSet = {
                                     onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ShadowSet))
                                 },
@@ -783,13 +757,7 @@ private fun ReadBookMenuSurface(
                                 onOpenHighlightRule = {
                                     onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.HighlightRuleConfig))
                                 },
-                                onOpenFontSelect = {
-                                    onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.FontSelect))
-                                },
-                                onOpenTitleFontSelect = {
-                                    onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TitleFontSelect))
-                                },
-                                onIntent = onIntent,
+                                sameTitleRemoved = state.sameTitleRemoved,
                             )
                         }
                     }
@@ -2039,7 +2007,6 @@ private fun OverflowDropdownMenu(
         expanded = expanded,
         onDismissRequest = onDismiss,
     ) { dismiss ->
-        var imageStyleExpanded by remember { mutableStateOf(false) }
 
         // Compact mode: moved from top bar buttons
         if (state.menuConfig.titleBarCompact) {
@@ -2076,23 +2043,7 @@ private fun OverflowDropdownMenu(
             PillDivider()
         }
 
-        // Source actions
-        if (!state.isLocalBook) {
-            RoundDropdownMenuItem(
-                text = stringResource(R.string.menu_refresh_all),
-                leadingIcon = menuIcon(Icons.Default.Replay),
-                onClick = { dismiss(); onIntent(ReadBookIntent.MenuRefreshAll) },
-            )
-        }
-
-        PillDivider()
-
-        // Content operations
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.bookmark_add),
-            leadingIcon = menuIcon(Icons.Default.Bookmark),
-            onClick = { dismiss(); onIntent(ReadBookIntent.AddBookmark) },
-        )
+        // 内容处理
         RoundDropdownMenuItem(
             text = stringResource(R.string.edit_content),
             leadingIcon = menuIcon(Icons.Default.Edit),
@@ -2102,59 +2053,27 @@ private fun OverflowDropdownMenu(
             },
         )
         RoundDropdownMenuItem(
-            text = stringResource(R.string.update_toc),
-            leadingIcon = menuIcon(Icons.Default.Replay),
-            onClick = { dismiss(); onIntent(ReadBookIntent.MenuUpdateToc) },
+            text = stringResource(R.string.bookmark_add),
+            leadingIcon = menuIcon(Icons.Default.Bookmark),
+            onClick = { dismiss(); onIntent(ReadBookIntent.AddBookmark) },
         )
         RoundDropdownMenuItem(
-            text = stringResource(R.string.simulated_reading),
-            leadingIcon = menuIcon(Icons.Default.AutoStories),
+            text = stringResource(R.string.highlight_rule_config),
+            leadingIcon = menuIcon(Icons.Default.Tune),
             onClick = {
                 dismiss()
-                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.SimulatedReading))
+                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.HighlightRuleConfig))
             },
         )
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.reverse_content),
-            leadingIcon = menuIcon(Icons.Default.SwapVert),
-            onClick = { dismiss(); onIntent(ReadBookIntent.MenuReverseContent) },
-        )
-
-        PillDivider()
-
-        // Checkable items
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.replace_rule_title),
-            leadingIcon = menuIcon(Icons.Default.FindReplace),
-            isSelected = state.useReplaceRule,
-            onClick = { onIntent(ReadBookIntent.MenuEnableReplace) },
-        )
         RoundDropdownMenuItem(
             text = stringResource(R.string.replace_rule_title_setting),
             leadingIcon = menuIcon(Icons.Default.Settings),
             onClick = { dismiss(); onIntent(ReadBookIntent.MenuSettingReplace) },
         )
         RoundDropdownMenuItem(
-            text = stringResource(R.string.effective_replaces),
-            leadingIcon = menuIcon(Icons.Default.Rule),
-            onClick = {
-                dismiss()
-                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.EffectiveReplaces))
-            },
-        )
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.content_processes),
-            leadingIcon = menuIcon(Icons.Default.Edit),
-            onClick = {
-                dismiss()
-                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ContentProcesses))
-            },
-        )
-        RoundDropdownMenuItem(
-            text = stringResource(R.string.same_title_removed),
-            leadingIcon = menuIcon(Icons.Default.CleanHands),
-            isSelected = state.sameTitleRemoved,
-            onClick = { onIntent(ReadBookIntent.MenuSameTitleRemoved) },
+            text = stringResource(R.string.reverse_content),
+            leadingIcon = menuIcon(Icons.Default.SwapVert),
+            onClick = { dismiss(); onIntent(ReadBookIntent.MenuReverseContent) },
         )
         RoundDropdownMenuItem(
             text = stringResource(R.string.re_segment),
@@ -2162,8 +2081,6 @@ private fun OverflowDropdownMenu(
             isSelected = state.reSegment,
             onClick = { onIntent(ReadBookIntent.MenuReSegment) },
         )
-
-        // EPUB
         if (state.isEpub) {
             RoundDropdownMenuItem(
                 text = stringResource(R.string.del_ruby_tag),
@@ -2181,47 +2098,7 @@ private fun OverflowDropdownMenu(
 
         PillDivider()
 
-        // Config
-        Box {
-            RoundDropdownMenuItem(
-                text = stringResource(R.string.image_style),
-                leadingIcon = menuIcon(Icons.Default.Image),
-                onClick = { imageStyleExpanded = true },
-            )
-            RoundDropdownMenu(
-                expanded = imageStyleExpanded,
-                onDismissRequest = { imageStyleExpanded = false },
-            ) { subDismiss ->
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.btn_default_s),
-                    onClick = {
-                        subDismiss()
-                        onIntent(ReadBookIntent.MenuImageStyle(Book.imgStyleDefault))
-                    },
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.image_style_full),
-                    onClick = {
-                        subDismiss()
-                        onIntent(ReadBookIntent.MenuImageStyle(Book.imgStyleFull))
-                    },
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.image_style_text),
-                    onClick = {
-                        subDismiss()
-                        onIntent(ReadBookIntent.MenuImageStyle(Book.imgStyleText))
-                    },
-                )
-                RoundDropdownMenuItem(
-                    text = stringResource(R.string.image_style_single),
-                    onClick = {
-                        subDismiss()
-                        onIntent(ReadBookIntent.MenuImageStyle(Book.imgStyleSingle))
-                    },
-                )
-            }
-        }
+        // 阅读设置
         RoundDropdownMenuItem(
             text = stringResource(R.string.book_page_anim),
             leadingIcon = menuIcon(Icons.Default.Animation),
@@ -2231,15 +2108,27 @@ private fun OverflowDropdownMenu(
             },
         )
         RoundDropdownMenuItem(
-            text = stringResource(R.string.config_btn),
-            leadingIcon = menuIcon(Icons.Default.Extension),
+            text = stringResource(R.string.simulated_reading),
+            leadingIcon = menuIcon(Icons.Default.AutoStories),
             onClick = {
                 dismiss()
-                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ToolButtonConfig))
+                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.SimulatedReading))
             },
         )
 
-        // Progress sync
+        PillDivider()
+
+        // 书源
+        if (!state.isLocalBook) {
+            RoundDropdownMenuItem(
+                text = stringResource(R.string.menu_refresh_all),
+                leadingIcon = menuIcon(Icons.Default.Replay),
+                onClick = { dismiss(); onIntent(ReadBookIntent.MenuRefreshAll) },
+            )
+            PillDivider()
+        }
+
+        // 进度同步
         if (state.isReadingProgressSyncConfigured) {
             RoundDropdownMenuItem(
                 text = stringResource(R.string.get_book_progress),
@@ -2251,10 +2140,18 @@ private fun OverflowDropdownMenu(
                 leadingIcon = menuIcon(Icons.Default.Sync),
                 onClick = { dismiss(); onIntent(ReadBookIntent.MenuCoverProgress) },
             )
+            PillDivider()
         }
 
-        PillDivider()
-
+        // 其他
+        RoundDropdownMenuItem(
+            text = stringResource(R.string.config_btn),
+            leadingIcon = menuIcon(Icons.Default.Extension),
+            onClick = {
+                dismiss()
+                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ToolButtonConfig))
+            },
+        )
         RoundDropdownMenuItem(
             text = stringResource(R.string.log),
             leadingIcon = menuIcon(Icons.Default.BugReport),

+ 6 - 1
app/src/main/java/io/legado/app/ui/book/read/ReadBookRouteScreen.kt

@@ -367,7 +367,12 @@ fun ReadBookRouteScreen(
                             ReadBookEffect.OpenTtsEnginesAndVoices -> onOpenTtsEnginesAndVoices()
                             ReadBookEffect.OpenTtsCache -> onOpenTtsCache()
                             is ReadBookEffect.MenuSettingReplace -> {
-                                replaceLauncher.launch(Intent(context, ReplaceRuleActivity::class.java))
+                                replaceLauncher.launch(
+                                    ReplaceRuleActivity.startIntent(
+                                        context = context,
+                                        bookUrl = ReadBook.book?.bookUrl
+                                    )
+                                )
                             }
                             is ReadBookEffect.TextActionReplace -> {
                                 val scopes = arrayListOf<String>()

+ 2 - 2
app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt

@@ -184,7 +184,7 @@ fun ReadBookScreen(
         onReplaceRuleChanged = { onIntent(ReadBookIntent.ReplaceRuleChanged) },
         onNavigateToTextEffects = {
             onIntent(ReadBookIntent.DismissSheet)
-            onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.TextTitle))
+            onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.TypographyConfig))
         },
         onOpenContentProcesses = {
             onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ContentProcesses))
@@ -532,7 +532,7 @@ fun ReadBookScreen(
         is ReadBookSheet.BookNavigation -> Unit
 
         is ReadBookSheet.InfoConfig -> {
-            // Integrated into ReadStyleSheet's HeaderFooterPage
+            // Integrated into TypographyPage
             LaunchedEffect(state.activeSheet) {
                 onIntent(ReadBookIntent.DismissSheet)
             }

+ 18 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -2501,6 +2501,18 @@ class ReadBookViewModel(
         footerPaddingBottom = ReadBookConfig.footerPaddingBottom,
         footerPaddingLeft = ReadBookConfig.footerPaddingLeft,
         footerPaddingRight = ReadBookConfig.footerPaddingRight,
+        headerFont = ReadBookConfig.headerFont,
+        footerFont = ReadBookConfig.footerFont,
+        headerFontSize = ReadBookConfig.headerFontSize,
+        footerFontSize = ReadBookConfig.footerFontSize,
+        applyHeaderStyle = ReadBookConfig.applyHeaderStyle,
+        tipDividerColor = ReadBookConfig.tipDividerColor,
+        tipHeaderColor = ReadBookConfig.tipHeaderColor,
+        tipHeaderColorNight = ReadBookConfig.tipHeaderColorNight,
+        tipFooterColor = ReadBookConfig.tipFooterColor,
+        tipFooterColorNight = ReadBookConfig.tipFooterColorNight,
+        textFullJustify = readSettingsRepository.currentSettings.textFullJustify,
+        textBottomJustify = readSettingsRepository.currentSettings.textBottomJustify,
         configNames = readBookStyleConfigRepository.currentState.items.map { it.name }
             .filter { it.isNotBlank() }
             .toImmutableList(),
@@ -4981,6 +4993,9 @@ class ReadBookViewModel(
             is ConfigUpdate.CustomTipFooterRight,
             is ConfigUpdate.HeaderFont,
             is ConfigUpdate.HeaderFontSize,
+            is ConfigUpdate.FooterFont,
+            is ConfigUpdate.FooterFontSize,
+            is ConfigUpdate.ApplyHeaderStyle,
             is ConfigUpdate.TipHeaderColor,
             is ConfigUpdate.TipHeaderColorNight,
             is ConfigUpdate.TipFooterColor,
@@ -5639,6 +5654,7 @@ class ReadBookViewModel(
         is ConfigUpdate.TipFooterMiddle -> intMutation(ReadStyleIntKey.TipFooterMiddle, value)
         is ConfigUpdate.TipFooterRight -> intMutation(ReadStyleIntKey.TipFooterRight, value)
         is ConfigUpdate.HeaderFont -> stringMutation(ReadStyleStringKey.HeaderFont, path)
+        is ConfigUpdate.FooterFont -> stringMutation(ReadStyleStringKey.FooterFont, path)
         is ConfigUpdate.CustomTipHeaderLeft ->
             stringMutation(ReadStyleStringKey.CustomTipHeaderLeft, value)
         is ConfigUpdate.CustomTipHeaderMiddle ->
@@ -5652,6 +5668,8 @@ class ReadBookViewModel(
         is ConfigUpdate.CustomTipFooterRight ->
             stringMutation(ReadStyleStringKey.CustomTipFooterRight, value)
         is ConfigUpdate.HeaderFontSize -> intMutation(ReadStyleIntKey.HeaderFontSize, value)
+        is ConfigUpdate.FooterFontSize -> intMutation(ReadStyleIntKey.FooterFontSize, value)
+        is ConfigUpdate.ApplyHeaderStyle -> booleanMutation(ReadStyleBooleanKey.ApplyHeaderStyle, value)
         is ConfigUpdate.TipHeaderColor -> colorMutation(ReadStyleColorKey.TipHeader, color)
         is ConfigUpdate.TipHeaderColorNight ->
             colorMutation(ReadStyleColorKey.TipHeaderNight, color)

+ 12 - 0
app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt

@@ -418,6 +418,18 @@ class PageView(
             view.textSize = customTextSize
             view.batteryMode = BatteryView.BatteryMode.NO_BATTERY
         }
+
+        // 当页脚不应用页眉字体样式时,覆盖页脚视图的字体和字号
+        if (!ReadBookConfig.applyHeaderStyle) {
+            val footerTypeface = loadTypeface(ReadBookConfig.footerFont) ?: tipTypeface
+            val footerTextSize = ReadBookConfig.footerFontSize.toFloat()
+            listOf(tvFooterLeft, tvFooterMiddle, tvFooterRight).forEach { view ->
+                if (view.tag != null) {
+                    view.typeface = footerTypeface
+                    view.textSize = footerTextSize
+                }
+            }
+        }
     }
 
     /**

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

@@ -23,10 +23,12 @@ import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.DarkMode
+import androidx.compose.material.icons.filled.FormatUnderlined
 import androidx.compose.material.icons.filled.GridView
+import androidx.compose.material.icons.filled.Layers
 import androidx.compose.material.icons.filled.LightMode
-import androidx.compose.material.icons.filled.SpaceBar
-import androidx.compose.material.icons.filled.TextFields
+import androidx.compose.material.icons.filled.Tablet
+import androidx.compose.material.icons.filled.Translate
 import androidx.compose.material.icons.filled.Visibility
 import androidx.compose.material3.Icon
 import androidx.compose.material3.PlainTooltip
@@ -45,12 +47,14 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringArrayResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.core.graphics.toColorInt
 import coil.compose.AsyncImage
 import io.legado.app.R
+import io.legado.app.data.repository.ReadPreferences
 import io.legado.app.domain.model.settings.ReadStyleItem
 import io.legado.app.help.config.ReadStyleResolver
 import io.legado.app.model.ReadBook
@@ -76,13 +80,14 @@ fun GlobalThemePage(
     onToggleDayNight: () -> Unit,
     eyeProtectionEnabled: Boolean,
     onOpenBgTextConfig: (Int) -> Unit,
-    onOpenTextTitle: () -> Unit,
-    onOpenPaddingConfig: () -> Unit,
+    onOpenUnderlineConfig: () -> Unit,
+    onOpenShadowSet: () -> Unit,
     onShareLayoutChange: (Boolean) -> Unit,
     onStyleSelect: (Int) -> Unit,
     modifier: Modifier = Modifier,
     onIntent: (ReadBookIntent) -> Unit,
     styleConfig: ReadBookStyleConfig = ReadBookStyleConfig(),
+    preferences: ReadPreferences = ReadPreferences(),
 ) {
     // Derive values directly from styleConfig (reactive state)
     val textSize = styleConfig.textSize
@@ -90,6 +95,8 @@ fun GlobalThemePage(
     val styleSelect = styleConfig.styleSelect
     val shareLayout = styleConfig.shareLayout
     val isNightTheme = LegadoTheme.isDark
+    val chineseConverterType = preferences.chineseConverterType
+    val doubleHorizontalPage = preferences.doubleHorizontalPage
 
     val configList = styleConfig.styleItems
 
@@ -113,7 +120,7 @@ fun GlobalThemePage(
                 },
             )
             NormalCard(
-                onClick = onOpenTextTitle,
+                onClick = onOpenUnderlineConfig,
                 modifier = Modifier
                     .height(56.dp)
                     .aspectRatio(1f),
@@ -125,8 +132,27 @@ fun GlobalThemePage(
                     modifier = Modifier.fillMaxSize(),
                 ) {
                     Icon(
-                        imageVector = Icons.Default.TextFields,
-                        contentDescription = stringResource(R.string.read_config_text_effects),
+                        imageVector = Icons.Default.FormatUnderlined,
+                        contentDescription = stringResource(R.string.use_underline),
+                        tint = LegadoTheme.colorScheme.onSurfaceVariant
+                    )
+                }
+            }
+            NormalCard(
+                onClick = onOpenShadowSet,
+                modifier = Modifier
+                    .height(56.dp)
+                    .aspectRatio(1f),
+                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+                cornerRadius = 12.dp
+            ) {
+                Box(
+                    contentAlignment = Alignment.Center,
+                    modifier = Modifier.fillMaxSize(),
+                ) {
+                    Icon(
+                        imageVector = Icons.Default.Layers,
+                        contentDescription = stringResource(R.string.text_shadow_set),
                         tint = LegadoTheme.colorScheme.onSurfaceVariant
                     )
                 }
@@ -320,6 +346,10 @@ fun GlobalThemePage(
         val currentPageAnimDisplay =
             pageAnimEntries.getOrNull(pageAnimEntryValues.indexOf(pageAnim.toString())) ?: ""
 
+        var showDualPageMenu by remember { mutableStateOf(false) }
+        val doublePageEntries = stringArrayResource(R.array.double_page_title)
+        val doublePageValues = stringArrayResource(R.array.double_page_value)
+
         Row(
             modifier = Modifier.fillMaxWidth(),
             horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -356,23 +386,101 @@ fun GlobalThemePage(
                     }
                 }
             }
-            NormalCard(
-                onClick = onOpenPaddingConfig,
-                modifier = Modifier
-                    .height(56.dp)
-                    .aspectRatio(1f),
-                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
-                cornerRadius = 12.dp
-            ) {
-                Box(
-                    contentAlignment = Alignment.Center,
-                    modifier = Modifier.fillMaxSize(),
+            // 中文简繁互转 — dropdown menu
+            Box {
+                var showChineseConvertMenu by remember { mutableStateOf(false) }
+                val chineseConvertEntries = stringArrayResource(R.array.chinese_mode)
+                val chineseConvertValues = remember { arrayOf("0", "1", "2") }
+                NormalCard(
+                    onClick = { showChineseConvertMenu = true },
+                    modifier = Modifier
+                        .height(56.dp)
+                        .aspectRatio(1f),
+                    containerColor = if (chineseConverterType > 0) {
+                        LegadoTheme.colorScheme.secondaryContainer
+                    } else {
+                        LegadoTheme.colorScheme.surfaceContainerLow
+                    },
+                    cornerRadius = 12.dp
                 ) {
-                    Icon(
-                        imageVector = Icons.Default.SpaceBar,
-                        contentDescription = stringResource(R.string.padding),
-                        tint = LegadoTheme.colorScheme.onSurfaceVariant
-                    )
+                    Box(
+                        contentAlignment = Alignment.Center,
+                        modifier = Modifier.fillMaxSize(),
+                    ) {
+                        Icon(
+                            imageVector = Icons.Default.Translate,
+                            contentDescription = stringResource(R.string.chinese_converter),
+                            tint = if (chineseConverterType > 0) {
+                                LegadoTheme.colorScheme.onSecondaryContainer
+                            } else {
+                                LegadoTheme.colorScheme.onSurfaceVariant
+                            }
+                        )
+                    }
+                }
+                RoundDropdownMenu(
+                    expanded = showChineseConvertMenu,
+                    onDismissRequest = { showChineseConvertMenu = false },
+                ) { dismiss ->
+                    chineseConvertEntries.forEachIndexed { index, display ->
+                        RoundDropdownMenuItem(
+                            text = display,
+                            isSelected = chineseConvertValues[index] == chineseConverterType.toString(),
+                            onClick = {
+                                onIntent(ReadBookIntent.UpdateConfig(
+                                    ConfigUpdate.ChineseConverterType(chineseConvertValues[index].toInt())
+                                ))
+                                dismiss()
+                            },
+                        )
+                    }
+                }
+            }
+            // 平板/横屏双页 — dropdown menu
+            Box {
+                NormalCard(
+                    onClick = { showDualPageMenu = true },
+                    modifier = Modifier
+                        .height(56.dp)
+                        .aspectRatio(1f),
+                    containerColor = if (doubleHorizontalPage != "0") {
+                        LegadoTheme.colorScheme.secondaryContainer
+                    } else {
+                        LegadoTheme.colorScheme.surfaceContainerLow
+                    },
+                    cornerRadius = 12.dp
+                ) {
+                    Box(
+                        contentAlignment = Alignment.Center,
+                        modifier = Modifier.fillMaxSize(),
+                    ) {
+                        Icon(
+                            imageVector = Icons.Default.Tablet,
+                            contentDescription = stringResource(R.string.double_page_horizontal),
+                            tint = if (doubleHorizontalPage != "0") {
+                                LegadoTheme.colorScheme.onSecondaryContainer
+                            } else {
+                                LegadoTheme.colorScheme.onSurfaceVariant
+                            }
+                        )
+                    }
+                }
+                RoundDropdownMenu(
+                    expanded = showDualPageMenu,
+                    onDismissRequest = { showDualPageMenu = false },
+                ) { dismiss ->
+                    doublePageEntries.forEachIndexed { index, display ->
+                        RoundDropdownMenuItem(
+                            text = display,
+                            isSelected = doublePageValues[index] == doubleHorizontalPage,
+                            onClick = {
+                                onIntent(ReadBookIntent.UpdateConfig(
+                                    ConfigUpdate.DoubleHorizontalPage(doublePageValues[index])
+                                ))
+                                dismiss()
+                            },
+                        )
+                    }
                 }
             }
         }

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

@@ -1,699 +0,0 @@
-package io.legado.app.ui.book.read.sheet
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.expandVertically
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.shrinkVertically
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ChevronRight
-import androidx.compose.material.icons.filled.ExpandMore
-import androidx.compose.material.icons.filled.TextFields
-import androidx.compose.material3.Icon
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateMapOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clipToBounds
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringArrayResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import androidx.core.net.toUri
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import io.legado.app.R
-import io.legado.app.data.repository.ReadSettingsRepository
-import io.legado.app.help.config.ReadBookConfig
-import io.legado.app.ui.book.read.ConfigUpdate
-import io.legado.app.ui.book.read.ReadBookIntent
-import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.FontFolderState
-import io.legado.app.ui.widget.components.FontSelectSheet
-import io.legado.app.ui.widget.components.dialog.CustomTipDialog
-import io.legado.app.ui.widget.components.SectionTitle
-import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
-import io.legado.app.ui.widget.components.pager.pagerHeight
-import io.legado.app.ui.widget.components.pager.rememberPagerAnimatedHeight
-import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
-import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem
-import io.legado.app.ui.widget.components.settingItem.TinyColorModeSettingItem
-import io.legado.app.ui.widget.components.settingItem.TinyColorSettingItem
-import io.legado.app.ui.widget.components.settingItem.TinyDropdownSettingItem
-import io.legado.app.ui.widget.components.settingItem.TinySliderSettingItem
-import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem
-import io.legado.app.ui.widget.components.tabRow.CardTabRow
-import io.legado.app.utils.getCompatColor
-import kotlinx.coroutines.launch
-import org.koin.compose.koinInject
-
-// Color picker IDs
-private const val COLOR_HEADER = 7
-private const val COLOR_FOOTER = 8
-private const val COLOR_DIVIDER = 9
-private const val COLOR_HEADER_NIGHT = 10
-private const val COLOR_FOOTER_NIGHT = 11
-
-@Composable
-internal fun HeaderFooterPage(
-    modifier: Modifier = Modifier,
-    onIntent: (ReadBookIntent) -> Unit,
-) {
-    val context = LocalContext.current
-    val scope = rememberCoroutineScope()
-
-    val tabTitles = listOf(
-        stringResource(R.string.header),
-        stringResource(R.string.footer),
-        stringResource(R.string.general),
-    )
-    val pagerState = rememberPagerState(pageCount = { 3 })
-    var selectedTab by remember { mutableIntStateOf(0) }
-    var clickScrollCount by remember { mutableIntStateOf(0) }
-    val childPagerNestedScrollConnection = rememberPagerFlingPassThroughConnection(
-        state = pagerState,
-        orientation = Orientation.Horizontal,
-    )
-
-    val pageHeights = remember { mutableStateMapOf<Int, Int>() }
-    val animatedHeight by rememberPagerAnimatedHeight(pagerState, pageHeights)
-
-    // Header state
-    var headerMode by remember { mutableIntStateOf(ReadBookConfig.headerMode) }
-    var headerLeft by remember { mutableIntStateOf(ReadBookConfig.tipHeaderLeft) }
-    var headerMiddle by remember { mutableIntStateOf(ReadBookConfig.tipHeaderMiddle) }
-    var headerRight by remember { mutableIntStateOf(ReadBookConfig.tipHeaderRight) }
-
-    // Footer state
-    var footerMode by remember { mutableIntStateOf(ReadBookConfig.footerMode) }
-    var footerLeft by remember { mutableIntStateOf(ReadBookConfig.tipFooterLeft) }
-    var footerMiddle by remember { mutableIntStateOf(ReadBookConfig.tipFooterMiddle) }
-    var footerRight by remember { mutableIntStateOf(ReadBookConfig.tipFooterRight) }
-
-    // 对哪个位置正在编辑自定义模板(null 表示未打开弹窗)
-    var editingCustomTarget by remember { mutableStateOf<CustomTipTarget?>(null) }
-
-    // Line toggles
-    var showHeaderLine by remember { mutableStateOf(ReadBookConfig.showHeaderLine) }
-    var showFooterLine by remember { mutableStateOf(ReadBookConfig.showFooterLine) }
-
-    // Global state
-    var headerFontSize by remember { mutableIntStateOf(ReadBookConfig.headerFontSize) }
-
-    var showColorPicker by remember { mutableStateOf(false) }
-    var colorPickerId by remember { mutableIntStateOf(0) }
-    var colorPickerInitial by remember { mutableIntStateOf(0) }
-    var showFontSelect by remember { mutableStateOf(false) }
-
-    var expandHeaderPadding by remember { mutableStateOf(false) }
-    var expandFooterPadding by remember { mutableStateOf(false) }
-
-    val headerScrollState = rememberScrollState()
-    val footerScrollState = rememberScrollState()
-
-    LaunchedEffect(expandHeaderPadding, headerScrollState.maxValue) {
-        if (expandHeaderPadding) {
-            headerScrollState.scrollTo(headerScrollState.maxValue)
-        }
-    }
-
-    LaunchedEffect(expandFooterPadding, footerScrollState.maxValue) {
-        if (expandFooterPadding) {
-            footerScrollState.scrollTo(footerScrollState.maxValue)
-        }
-    }
-
-    DisposableEffect(Unit) {
-        onDispose {
-            onIntent(ReadBookIntent.SaveReadStyleConfig)
-        }
-    }
-
-    // Header padding state
-    var headerPaddingTop by remember { mutableFloatStateOf(ReadBookConfig.headerPaddingTop.toFloat()) }
-    var headerPaddingBottom by remember { mutableFloatStateOf(ReadBookConfig.headerPaddingBottom.toFloat()) }
-    var headerPaddingLeft by remember { mutableFloatStateOf(ReadBookConfig.headerPaddingLeft.toFloat()) }
-    var headerPaddingRight by remember { mutableFloatStateOf(ReadBookConfig.headerPaddingRight.toFloat()) }
-
-    // Footer padding state
-    var footerPaddingTop by remember { mutableFloatStateOf(ReadBookConfig.footerPaddingTop.toFloat()) }
-    var footerPaddingBottom by remember { mutableFloatStateOf(ReadBookConfig.footerPaddingBottom.toFloat()) }
-    var footerPaddingLeft by remember { mutableFloatStateOf(ReadBookConfig.footerPaddingLeft.toFloat()) }
-    var footerPaddingRight by remember { mutableFloatStateOf(ReadBookConfig.footerPaddingRight.toFloat()) }
-
-    val tipNames = remember { ReadBookConfig.tipNames }
-    val tipValues = remember { ReadBookConfig.tipValues }
-
-    fun clearRepeat(repeat: Int) {
-        if (repeat == ReadBookConfig.tipNone) return
-        if (headerLeft == repeat) {
-            headerLeft = ReadBookConfig.tipNone
-            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderLeft(ReadBookConfig.tipNone)))
-        }
-        if (headerMiddle == repeat) {
-            headerMiddle = ReadBookConfig.tipNone
-            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderMiddle(ReadBookConfig.tipNone)))
-        }
-        if (headerRight == repeat) {
-            headerRight = ReadBookConfig.tipNone
-            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderRight(ReadBookConfig.tipNone)))
-        }
-        if (footerLeft == repeat) {
-            footerLeft = ReadBookConfig.tipNone
-            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterLeft(ReadBookConfig.tipNone)))
-        }
-        if (footerMiddle == repeat) {
-            footerMiddle = ReadBookConfig.tipNone
-            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterMiddle(ReadBookConfig.tipNone)))
-        }
-        if (footerRight == repeat) {
-            footerRight = ReadBookConfig.tipNone
-            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterRight(ReadBookConfig.tipNone)))
-        }
-    }
-
-    fun applyTipValue(target: CustomTipTarget, value: Int) {
-        when (target) {
-            CustomTipTarget.HEADER_LEFT -> {
-                headerLeft = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderLeft(value)))
-            }
-            CustomTipTarget.HEADER_MIDDLE -> {
-                headerMiddle = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderMiddle(value)))
-            }
-            CustomTipTarget.HEADER_RIGHT -> {
-                headerRight = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderRight(value)))
-            }
-            CustomTipTarget.FOOTER_LEFT -> {
-                footerLeft = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterLeft(value)))
-            }
-            CustomTipTarget.FOOTER_MIDDLE -> {
-                footerMiddle = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterMiddle(value)))
-            }
-            CustomTipTarget.FOOTER_RIGHT -> {
-                footerRight = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterRight(value)))
-            }
-        }
-    }
-
-    /**
-     * 当某个位置的 tip 被切换时统一处理:清空同值重复、更新本地 state、派发 ConfigUpdate,
-     * 必要时自动弹出自定义模板编辑弹窗。
-     */
-    fun handleTipChange(target: CustomTipTarget, value: Int) {
-        if (value == ReadBookConfig.tipCustom) {
-            editingCustomTarget = target
-            return
-        }
-        clearRepeat(value)
-        applyTipValue(target, value)
-    }
-
-    LaunchedEffect(pagerState) {
-        snapshotFlow { pagerState.currentPage }.collect { page ->
-            if (clickScrollCount == 0) {
-                selectedTab = page
-            }
-        }
-    }
-
-    Column(
-        modifier = modifier
-            .fillMaxWidth()
-    ) {
-        CardTabRow(
-            tabTitles = tabTitles,
-            selectedTabIndex = selectedTab,
-            onTabSelected = { index ->
-                selectedTab = index
-                clickScrollCount++
-                scope.launch {
-                    try {
-                        pagerState.animateScrollToPage(
-                            page = index,
-                            animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
-                        )
-                    } finally {
-                        clickScrollCount = (clickScrollCount - 1).coerceAtLeast(0)
-                    }
-                }
-            },
-            modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
-        )
-        HorizontalPager(
-            state = pagerState,
-            verticalAlignment = Alignment.Top,
-            pageNestedScrollConnection = childPagerNestedScrollConnection,
-            modifier = Modifier
-                .fillMaxWidth()
-                .clipToBounds()
-                .pagerHeight(animatedHeight),
-        ) { page ->
-            Box(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .onSizeChanged { size ->
-                        pageHeights[page] = size.height
-                    }
-            ) {
-                when (page) {
-                0 -> {
-                    // Header tab
-                    Column(
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .padding(horizontal = 16.dp)
-                            .verticalScroll(headerScrollState),
-                    ) {
-                        TinySwitchSettingItem(
-                            title = stringResource(R.string.showLine),
-                            checked = showHeaderLine,
-                            onCheckedChange = {
-                                showHeaderLine = it
-                                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShowHeaderLine(it)))
-                            },
-                        )
-                        val headerModes = ReadBookConfig.getHeaderModes(context)
-                        TinyDropdownSettingItem(
-                            title = stringResource(R.string.header),
-                            selectedValue = headerMode.toString(),
-                            displayEntries = headerModes.values.toTypedArray(),
-                            entryValues = headerModes.keys.map { it.toString() }.toTypedArray(),
-                            onValueChange = {
-                                headerMode = it.toInt()
-                                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderMode(headerMode)))
-                            },
-                        )
-                        TipPositionDropdown(
-                            label = stringResource(R.string.left),
-                            value = headerLeft,
-                            tipNames = tipNames,
-                            tipValues = tipValues,
-                            onValueChange = { handleTipChange(CustomTipTarget.HEADER_LEFT, it) },
-                        )
-                        TipPositionDropdown(
-                            label = stringResource(R.string.middle),
-                            value = headerMiddle,
-                            tipNames = tipNames,
-                            tipValues = tipValues,
-                            onValueChange = { handleTipChange(CustomTipTarget.HEADER_MIDDLE, it) },
-                        )
-                        TipPositionDropdown(
-                            label = stringResource(R.string.right),
-                            value = headerRight,
-                            tipNames = tipNames,
-                            tipValues = tipValues,
-                            onValueChange = { handleTipChange(CustomTipTarget.HEADER_RIGHT, it) },
-                        )
-                        TinyColorModeSettingItem(
-                            title = stringResource(R.string.header_color),
-                            dayColor = if (ReadBookConfig.tipHeaderColor != 0) {
-                                ReadBookConfig.tipHeaderColor
-                            } else {
-                                ReadBookConfig.textColor
-                            },
-                            nightColor = if (ReadBookConfig.tipHeaderColorNight != 0) {
-                                ReadBookConfig.tipHeaderColorNight
-                            } else {
-                                ReadBookConfig.textColorNight
-                            },
-                            onClickColor = { isNight ->
-                                if (isNight) {
-                                    colorPickerId = COLOR_HEADER_NIGHT
-                                    colorPickerInitial = if (ReadBookConfig.tipHeaderColorNight != 0) {
-                                        ReadBookConfig.tipHeaderColorNight
-                                    } else {
-                                        ReadBookConfig.textColorNight
-                                    }
-                                } else {
-                                    colorPickerId = COLOR_HEADER
-                                    colorPickerInitial = if (ReadBookConfig.tipHeaderColor != 0) {
-                                        ReadBookConfig.tipHeaderColor
-                                    } else {
-                                        ReadBookConfig.textColor
-                                    }
-                                }
-                                showColorPicker = true
-                            },
-                        )
-
-                        TinyClickableSettingItem(
-                            title = stringResource(R.string.padding),
-                            description = stringResource(
-                                R.string.padding_format,
-                                headerPaddingTop.toInt(),
-                                headerPaddingBottom.toInt(),
-                                headerPaddingLeft.toInt(),
-                                headerPaddingRight.toInt(),
-                            ),
-                            trailingContent = {
-                                Icon(
-                                    imageVector = if (expandHeaderPadding) Icons.Default.ExpandMore else Icons.Default.ChevronRight,
-                                    contentDescription = null,
-                                    tint = LegadoTheme.colorScheme.onSurfaceVariant,
-                                    modifier = Modifier.size(20.dp),
-                                )
-                            },
-                            onClick = {
-                                expandHeaderPadding = !expandHeaderPadding
-                            },
-                        )
-                        AnimatedVisibility(
-                            visible = expandHeaderPadding,
-                            enter = expandVertically() + fadeIn(),
-                            exit = shrinkVertically() + fadeOut(),
-                        ) {
-                            Column(modifier = Modifier.fillMaxWidth()) {
-                                PaddingSliders(
-                                    top = headerPaddingTop, bottom = headerPaddingBottom,
-                                    left = headerPaddingLeft, right = headerPaddingRight,
-                                    onTopChange = {
-                                        headerPaddingTop = it
-                                        onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingTop(it.toInt())))
-                                    },
-                                    onBottomChange = {
-                                        headerPaddingBottom = it
-                                        onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingBottom(it.toInt())))
-                                    },
-                                    onLeftChange = {
-                                        headerPaddingLeft = it
-                                        onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingLeft(it.toInt())))
-                                    },
-                                    onRightChange = {
-                                        headerPaddingRight = it
-                                        onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingRight(it.toInt())))
-                                    },
-                                )
-                            }
-                        }
-                    }
-                }
-
-                1 -> {
-                    // Footer tab
-                    Column(
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .padding(horizontal = 16.dp)
-                            .verticalScroll(footerScrollState),
-                    ) {
-                        TinySwitchSettingItem(
-                            title = stringResource(R.string.showLine),
-                            checked = showFooterLine,
-                            onCheckedChange = {
-                                showFooterLine = it
-                                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShowFooterLine(it)))
-                            },
-                        )
-                        val footerModes = ReadBookConfig.getFooterModes(context)
-                        TinyDropdownSettingItem(
-                            title = stringResource(R.string.footer),
-                            selectedValue = footerMode.toString(),
-                            displayEntries = footerModes.values.toTypedArray(),
-                            entryValues = footerModes.keys.map { it.toString() }.toTypedArray(),
-                            onValueChange = {
-                                footerMode = it.toInt()
-                                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterMode(footerMode)))
-                            },
-                        )
-                        TipPositionDropdown(
-                            label = stringResource(R.string.left),
-                            value = footerLeft,
-                            tipNames = tipNames,
-                            tipValues = tipValues,
-                            onValueChange = { handleTipChange(CustomTipTarget.FOOTER_LEFT, it) },
-                        )
-                        TipPositionDropdown(
-                            label = stringResource(R.string.middle),
-                            value = footerMiddle,
-                            tipNames = tipNames,
-                            tipValues = tipValues,
-                            onValueChange = { handleTipChange(CustomTipTarget.FOOTER_MIDDLE, it) },
-                        )
-                        TipPositionDropdown(
-                            label = stringResource(R.string.right),
-                            value = footerRight,
-                            tipNames = tipNames,
-                            tipValues = tipValues,
-                            onValueChange = { handleTipChange(CustomTipTarget.FOOTER_RIGHT, it) },
-                        )
-                        TinyColorModeSettingItem(
-                            title = stringResource(R.string.footer_color),
-                            dayColor = if (ReadBookConfig.tipFooterColor != 0) {
-                                ReadBookConfig.tipFooterColor
-                            } else {
-                                ReadBookConfig.textColor
-                            },
-                            nightColor = if (ReadBookConfig.tipFooterColorNight != 0) {
-                                ReadBookConfig.tipFooterColorNight
-                            } else {
-                                ReadBookConfig.textColorNight
-                            },
-                            onClickColor = { isNight ->
-                                if (isNight) {
-                                    colorPickerId = COLOR_FOOTER_NIGHT
-                                    colorPickerInitial = if (ReadBookConfig.tipFooterColorNight != 0) {
-                                        ReadBookConfig.tipFooterColorNight
-                                    } else {
-                                        ReadBookConfig.textColorNight
-                                    }
-                                } else {
-                                    colorPickerId = COLOR_FOOTER
-                                    colorPickerInitial = if (ReadBookConfig.tipFooterColor != 0) {
-                                        ReadBookConfig.tipFooterColor
-                                    } else {
-                                        ReadBookConfig.textColor
-                                    }
-                                }
-                                showColorPicker = true
-                            },
-                        )
-
-                        TinyClickableSettingItem(
-                            title = stringResource(R.string.padding),
-                            description = stringResource(
-                                R.string.padding_format,
-                                footerPaddingTop.toInt(),
-                                footerPaddingBottom.toInt(),
-                                footerPaddingLeft.toInt(),
-                                footerPaddingRight.toInt(),
-                            ),
-                            trailingContent = {
-                                Icon(
-                                    imageVector = if (expandFooterPadding) Icons.Default.ExpandMore else Icons.Default.ChevronRight,
-                                    contentDescription = null,
-                                    tint = LegadoTheme.colorScheme.onSurfaceVariant,
-                                    modifier = Modifier.size(20.dp),
-                                )
-                            },
-                            onClick = {
-                                expandFooterPadding = !expandFooterPadding
-                            },
-                        )
-                        AnimatedVisibility(
-                            visible = expandFooterPadding,
-                            enter = expandVertically() + fadeIn(),
-                            exit = shrinkVertically() + fadeOut(),
-                        ) {
-                            Column(modifier = Modifier.fillMaxWidth()) {
-                                PaddingSliders(
-                                    top = footerPaddingTop, bottom = footerPaddingBottom,
-                                    left = footerPaddingLeft, right = footerPaddingRight,
-                                    onTopChange = {
-                                        footerPaddingTop = it
-                                        onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingTop(it.toInt())))
-                                    },
-                                    onBottomChange = {
-                                        footerPaddingBottom = it
-                                        onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingBottom(it.toInt())))
-                                    },
-                                    onLeftChange = {
-                                        footerPaddingLeft = it
-                                        onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingLeft(it.toInt())))
-                                    },
-                                    onRightChange = {
-                                        footerPaddingRight = it
-                                        onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingRight(it.toInt())))
-                                    },
-                                )
-                            }
-                        }
-                    }
-                }
-
-                2 -> {
-                    // Global tab
-                    Column(
-                        modifier = Modifier
-                            .fillMaxWidth()
-                            .padding(horizontal = 16.dp)
-                            .verticalScroll(rememberScrollState()),
-                    ) {
-                        SectionTitle(stringResource(R.string.read_config_divider_line))
-                        TinyColorSettingItem(
-                            title = stringResource(R.string.tip_divider_color),
-                            colorValue = when (ReadBookConfig.tipDividerColor) {
-                                -1 -> context.getCompatColor(R.color.divider)
-                                0 -> ReadBookConfig.textColor
-                                else -> ReadBookConfig.tipDividerColor
-                            },
-                            onClick = {
-                                colorPickerId = COLOR_DIVIDER
-                                colorPickerInitial = when (ReadBookConfig.tipDividerColor) {
-                                    -1 -> context.getCompatColor(R.color.divider)
-                                    0 -> ReadBookConfig.textColor
-                                    else -> ReadBookConfig.tipDividerColor
-                                }
-                                showColorPicker = true
-                            },
-                        )
-
-                        SectionTitle(stringResource(R.string.text_typeface))
-                        TinyClickableSettingItem(
-                            title = stringResource(R.string.header_font),
-                            description = stringResource(R.string.select_font),
-                            imageVector = Icons.Default.TextFields,
-                            onClick = { showFontSelect = true },
-                        )
-                        TinySliderSettingItem(
-                            title = stringResource(R.string.header_font_size),
-                            value = headerFontSize.toFloat(),
-                            valueRange = 0f..100f,
-                            onValueChange = { value ->
-                                headerFontSize = value.toInt()
-                                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderFontSize(value.toInt())))
-                                onIntent(ReadBookIntent.SaveReadStyleConfig)
-                            },
-                        )
-                    }
-                }
-            }
-        }
-    }
-}
-
-    // Color picker
-    ColorPickerSheet(
-        show = showColorPicker,
-        initialColor = colorPickerInitial,
-        onDismissRequest = { showColorPicker = false },
-        onColorSelected = { color ->
-            when (colorPickerId) {
-                COLOR_HEADER -> {
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderColor(color)))
-                }
-
-                COLOR_HEADER_NIGHT -> {
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderColorNight(color)))
-                }
-
-                COLOR_FOOTER -> {
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterColor(color)))
-                }
-
-                COLOR_FOOTER_NIGHT -> {
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterColorNight(color)))
-                }
-
-                COLOR_DIVIDER -> {
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipDividerColor(color)))
-                }
-            }
-            showColorPicker = false
-        },
-    )
-
-    // Font selector for header/footer
-    val readSettingsRepository: ReadSettingsRepository = koinInject()
-    val preferences by readSettingsRepository.preferences.collectAsStateWithLifecycle(
-        initialValue = null
-    )
-    val fontFolderState = remember(preferences) {
-        val pref = preferences
-        if (pref == null) {
-            FontFolderState.Loading
-        } else {
-            FontFolderState.Loaded(pref.fontFolder.takeIf { it.isNotEmpty() }?.toUri())
-        }
-    }
-    val systemTypefaces = stringArrayResource(R.array.system_typefaces)
-
-    val editingCustomInitial: String = editingCustomTarget?.customTemplate.orEmpty()
-
-    CustomTipDialog(
-        show = editingCustomTarget != null,
-        initialTemplate = editingCustomInitial,
-        onConfirm = { template ->
-            editingCustomTarget?.let { target ->
-                applyTipValue(target, ReadBookConfig.tipCustom)
-                target.applyTemplate(template, onIntent)
-            }
-            editingCustomTarget = null
-        },
-        onDismissRequest = { editingCustomTarget = null },
-    )
-
-    FontSelectSheet(
-        show = showFontSelect,
-        title = stringResource(R.string.select_font),
-        folderState = fontFolderState,
-        selectedFontPath = ReadBookConfig.headerFont,
-        onDismissRequest = { showFontSelect = false },
-        onSelectFont = {
-            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderFont(it.uri.toString())))
-            onIntent(ReadBookIntent.SaveReadStyleConfig)
-            showFontSelect = false
-        },
-        onSelectSystemTypeface = {
-            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderFont("")))
-            onIntent(ReadBookIntent.SaveReadStyleConfig)
-            showFontSelect = false
-        },
-        onOpenFolderPicker = { /* handled by FontSelectSheet internally */ },
-        systemTypefaces = systemTypefaces,
-    )
-}
-
-@Composable
-internal fun TipPositionDropdown(
-    label: String,
-    value: Int,
-    tipNames: List<String>,
-    tipValues: Array<Int>,
-    onValueChange: (Int) -> Unit,
-) {
-    TinyDropdownSettingItem(
-        title = label,
-        selectedValue = value.toString(),
-        displayEntries = tipNames.toTypedArray(),
-        entryValues = tipValues.map { it.toString() }.toTypedArray(),
-        onValueChange = { onValueChange(it.toInt()) },
-    )
-}

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

@@ -72,21 +72,12 @@ fun MoreConfigSheet(
                 onReadBodyToLhChange = {
                     onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ReadBodyToLh(it)))
                 },
-                onTextFullJustifyChange = {
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TextFullJustify(it)))
-                },
-                onTextBottomJustifyChange = {
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TextBottomJustify(it)))
-                },
                 onAdaptSpecialStyleChange = {
                     onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.AdaptSpecialStyle(it)))
                 },
                 onUseZhLayoutChange = {
                     onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.UseZhLayout(it)))
                 },
-                onUseUnderlineChange = {
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.UseUnderlineGlobal(it)))
-                },
                 onOpenEyeProtectionConfig = {
                     onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.EyeProtection))
                 }
@@ -96,9 +87,6 @@ fun MoreConfigSheet(
             SectionTitle(stringResource(R.string.page_control))
             PageControlSettings(
                 preferences = preferences,
-                onDoubleHorizontalPageChange = {
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.DoubleHorizontalPage(it)))
-                },
                 onProgressBarBehaviorChange = {
                     onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ProgressBarBehavior(it)))
                 },
@@ -173,11 +161,8 @@ private fun ScreenSettings(
     onHideNavigationBarChange: (Boolean) -> Unit,
     onPaddingDisplayCutoutsChange: (Boolean) -> Unit,
     onReadBodyToLhChange: (Boolean) -> Unit,
-    onTextFullJustifyChange: (Boolean) -> Unit,
-    onTextBottomJustifyChange: (Boolean) -> Unit,
     onAdaptSpecialStyleChange: (Boolean) -> Unit,
     onUseZhLayoutChange: (Boolean) -> Unit,
-    onUseUnderlineChange: (Boolean) -> Unit,
     onOpenEyeProtectionConfig:() -> Unit,
 ) {
     val screenDirectionEntries = stringArrayResource(R.array.screen_direction_title)
@@ -219,16 +204,6 @@ private fun ScreenSettings(
         checked = preferences.readBodyToLh,
         onCheckedChange = onReadBodyToLhChange,
     )
-    TinySwitchSettingItem(
-        title = stringResource(R.string.text_full_justify),
-        checked = preferences.textFullJustify,
-        onCheckedChange = onTextFullJustifyChange,
-    )
-    TinySwitchSettingItem(
-        title = stringResource(R.string.text_bottom_justify),
-        checked = preferences.textBottomJustify,
-        onCheckedChange = onTextBottomJustifyChange,
-    )
     TinySwitchSettingItem(
         title = stringResource(R.string.adapt_special_style),
         checked = preferences.adaptSpecialStyle,
@@ -239,11 +214,6 @@ private fun ScreenSettings(
         checked = preferences.useZhLayout,
         onCheckedChange = onUseZhLayoutChange,
     )
-    TinySwitchSettingItem(
-        title = stringResource(R.string.use_underline),
-        checked = preferences.useUnderline,
-        onCheckedChange = onUseUnderlineChange,
-    )
     TinyClickableSettingItem(
         title = stringResource(R.string.eye_protection),
         onClick = onOpenEyeProtectionConfig,
@@ -253,25 +223,15 @@ private fun ScreenSettings(
 @Composable
 private fun PageControlSettings(
     preferences: ReadPreferences,
-    onDoubleHorizontalPageChange: (String) -> Unit,
     onProgressBarBehaviorChange: (String) -> Unit,
     onMouseWheelPageChange: (Boolean) -> Unit,
     onVolumeKeyPageChange: (Boolean) -> Unit,
     onVolumeKeyPageOnPlayChange: (Boolean) -> Unit,
     onKeyPageOnLongPressChange: (Boolean) -> Unit,
 ) {
-    val doublePageEntries = stringArrayResource(R.array.double_page_title)
-    val doublePageValues = stringArrayResource(R.array.double_page_value)
     val progressBarEntries = stringArrayResource(R.array.progress_bar_behavior_title)
     val progressBarValues = stringArrayResource(R.array.progress_bar_behavior_value)
 
-    TinyDropdownSettingItem(
-        title = stringResource(R.string.double_page_horizontal),
-        selectedValue = preferences.doubleHorizontalPage,
-        displayEntries = doublePageEntries,
-        entryValues = doublePageValues,
-        onValueChange = onDoubleHorizontalPageChange,
-    )
     TinyDropdownSettingItem(
         title = stringResource(R.string.progress_bar_behavior),
         selectedValue = preferences.progressBarBehavior,

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

@@ -1,250 +0,0 @@
-package io.legado.app.ui.book.read.sheet
-
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateMapOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clipToBounds
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import io.legado.app.R
-import io.legado.app.ui.book.read.ReadSheetConfigUiState
-import io.legado.app.ui.book.read.ConfigUpdate
-import io.legado.app.ui.book.read.ReadBookIntent
-import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
-import io.legado.app.ui.widget.components.pager.pagerHeight
-import io.legado.app.ui.widget.components.pager.rememberPagerAnimatedHeight
-import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
-import io.legado.app.ui.widget.components.settingItem.TinySliderSettingItem
-import io.legado.app.ui.widget.components.tabRow.CardTabRow
-import kotlinx.coroutines.launch
-
-@Composable
-fun PaddingConfigSheet(
-    config: ReadSheetConfigUiState,
-    onDismissRequest: () -> Unit,
-    onIntent: (ReadBookIntent) -> Unit,
-) {
-    AppModalBottomSheet(
-        show = true,
-        onDismissRequest = {
-            onIntent(ReadBookIntent.SaveReadStyleConfig)
-            onDismissRequest()
-        },
-        title = stringResource(R.string.padding),
-    ) {
-        PaddingConfigContent(
-            config = config,
-            onIntent = onIntent,
-            modifier = Modifier
-                .padding(bottom = 16.dp),
-        )
-    }
-}
-
-@Composable
-fun PaddingConfigContent(
-    config: ReadSheetConfigUiState,
-    onIntent: (ReadBookIntent) -> Unit,
-    modifier: Modifier = Modifier,
-) {
-    val scope = rememberCoroutineScope()
-    val tabTitles = listOf(
-        stringResource(R.string.main_body),
-        stringResource(R.string.header),
-        stringResource(R.string.footer),
-    )
-    val pagerState = rememberPagerState(pageCount = { 3 })
-    var selectedTab by remember { mutableIntStateOf(0) }
-    var clickScrollCount by remember { mutableIntStateOf(0) }
-    val childPagerNestedScrollConnection = rememberPagerFlingPassThroughConnection(
-        state = pagerState,
-        orientation = Orientation.Horizontal,
-    )
-    val pageHeights = remember { mutableStateMapOf<Int, Int>() }
-    val animatedHeight by rememberPagerAnimatedHeight(pagerState, pageHeights)
-
-    var paddingTop by remember(config.paddingTop) { mutableFloatStateOf(config.paddingTop.toFloat()) }
-    var paddingBottom by remember(config.paddingBottom) { mutableFloatStateOf(config.paddingBottom.toFloat()) }
-    var paddingLeft by remember(config.paddingLeft) { mutableFloatStateOf(config.paddingLeft.toFloat()) }
-    var paddingRight by remember(config.paddingRight) { mutableFloatStateOf(config.paddingRight.toFloat()) }
-
-    var headerPaddingTop by remember(config.headerPaddingTop) { mutableFloatStateOf(config.headerPaddingTop.toFloat()) }
-    var headerPaddingBottom by remember(config.headerPaddingBottom) { mutableFloatStateOf(config.headerPaddingBottom.toFloat()) }
-    var headerPaddingLeft by remember(config.headerPaddingLeft) { mutableFloatStateOf(config.headerPaddingLeft.toFloat()) }
-    var headerPaddingRight by remember(config.headerPaddingRight) { mutableFloatStateOf(config.headerPaddingRight.toFloat()) }
-
-    var footerPaddingTop by remember(config.footerPaddingTop) { mutableFloatStateOf(config.footerPaddingTop.toFloat()) }
-    var footerPaddingBottom by remember(config.footerPaddingBottom) { mutableFloatStateOf(config.footerPaddingBottom.toFloat()) }
-    var footerPaddingLeft by remember(config.footerPaddingLeft) { mutableFloatStateOf(config.footerPaddingLeft.toFloat()) }
-    var footerPaddingRight by remember(config.footerPaddingRight) { mutableFloatStateOf(config.footerPaddingRight.toFloat()) }
-
-    LaunchedEffect(pagerState) {
-        snapshotFlow { pagerState.currentPage }.collect { page ->
-            if (clickScrollCount == 0) selectedTab = page
-        }
-    }
-
-    Column(
-        modifier = modifier.fillMaxWidth(),
-    ) {
-        CardTabRow(
-            tabTitles = tabTitles,
-            selectedTabIndex = selectedTab,
-            onTabSelected = { index ->
-                selectedTab = index
-                clickScrollCount++
-                scope.launch {
-                    try {
-                        pagerState.animateScrollToPage(
-                            page = index,
-                            animationSpec = tween(300, easing = FastOutSlowInEasing),
-                        )
-                    } finally {
-                        clickScrollCount = (clickScrollCount - 1).coerceAtLeast(0)
-                    }
-                }
-            },
-            modifier = Modifier.padding(bottom = 8.dp),
-        )
-
-        HorizontalPager(
-            state = pagerState,
-            verticalAlignment = Alignment.Top,
-            pageNestedScrollConnection = childPagerNestedScrollConnection,
-            modifier = Modifier
-                .fillMaxWidth()
-                .clipToBounds()
-                .pagerHeight(animatedHeight),
-        ) { page ->
-            Box(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .onSizeChanged { pageHeights[page] = it.height },
-            ) {
-                when (page) {
-                    0 -> PaddingSliders(
-                        top = paddingTop,
-                        bottom = paddingBottom,
-                        left = paddingLeft,
-                        right = paddingRight,
-                        onTopChange = {
-                            paddingTop = it
-                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingTop(it.toInt())))
-                        },
-                        onBottomChange = {
-                            paddingBottom = it
-                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingBottom(it.toInt())))
-                        },
-                        onLeftChange = {
-                            paddingLeft = it
-                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingLeft(it.toInt())))
-                        },
-                        onRightChange = {
-                            paddingRight = it
-                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingRight(it.toInt())))
-                        },
-                    )
-
-                    1 -> PaddingSliders(
-                        top = headerPaddingTop,
-                        bottom = headerPaddingBottom,
-                        left = headerPaddingLeft,
-                        right = headerPaddingRight,
-                        onTopChange = {
-                            headerPaddingTop = it
-                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingTop(it.toInt())))
-                        },
-                        onBottomChange = {
-                            headerPaddingBottom = it
-                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingBottom(it.toInt())))
-                        },
-                        onLeftChange = {
-                            headerPaddingLeft = it
-                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingLeft(it.toInt())))
-                        },
-                        onRightChange = {
-                            headerPaddingRight = it
-                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingRight(it.toInt())))
-                        },
-                    )
-
-                    2 -> PaddingSliders(
-                        top = footerPaddingTop,
-                        bottom = footerPaddingBottom,
-                        left = footerPaddingLeft,
-                        right = footerPaddingRight,
-                        onTopChange = {
-                            footerPaddingTop = it
-                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingTop(it.toInt())))
-                        },
-                        onBottomChange = {
-                            footerPaddingBottom = it
-                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingBottom(it.toInt())))
-                        },
-                        onLeftChange = {
-                            footerPaddingLeft = it
-                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingLeft(it.toInt())))
-                        },
-                        onRightChange = {
-                            footerPaddingRight = it
-                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingRight(it.toInt())))
-                        },
-                    )
-                }
-            }
-        }
-    }
-}
-
-@Composable
-internal fun PaddingSliders(
-    top: Float, bottom: Float, left: Float, right: Float,
-    onTopChange: (Float) -> Unit, onBottomChange: (Float) -> Unit,
-    onLeftChange: (Float) -> Unit, onRightChange: (Float) -> Unit,
-) {
-    Column(modifier = Modifier.fillMaxWidth()) {
-        TinySliderSettingItem(
-            title = stringResource(R.string.padding_top),
-            value = top,
-            valueRange = 0f..300f,
-            onValueChange = onTopChange,
-        )
-        TinySliderSettingItem(
-            title = stringResource(R.string.padding_bottom),
-            value = bottom,
-            valueRange = 0f..300f,
-            onValueChange = onBottomChange,
-        )
-        TinySliderSettingItem(
-            title = stringResource(R.string.padding_left),
-            value = left,
-            valueRange = 0f..300f,
-            onValueChange = onLeftChange,
-        )
-        TinySliderSettingItem(
-            title = stringResource(R.string.padding_right),
-            value = right,
-            valueRange = 0f..300f,
-            onValueChange = onRightChange,
-        )
-    }
-}

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

@@ -38,11 +38,11 @@ import kotlinx.coroutines.launch
 
 @Composable
 fun ReadStyleContent(
-    onOpenPaddingConfig: () -> Unit,
-    onOpenHeaderFooterConfig: () -> Unit,
+    onOpenTypographyConfig: () -> Unit,
     onOpenMoreConfig: () -> Unit,
     onOpenBgTextConfig: (Int) -> Unit,
-    onOpenTextTitle: () -> Unit,
+    onOpenUnderlineConfig: () -> Unit,
+    onOpenShadowSet: () -> Unit,
     onOpenFontSelect: () -> Unit,
     onToggleDayNight: () -> Unit,
     onPageChanged: (Int) -> Unit = {},
@@ -97,8 +97,8 @@ fun ReadStyleContent(
                         onToggleDayNight = onToggleDayNight,
                         eyeProtectionEnabled = eyeProtectionEnabled,
                         onOpenBgTextConfig = onOpenBgTextConfig,
-                        onOpenTextTitle = onOpenTextTitle,
-                        onOpenPaddingConfig = onOpenPaddingConfig,
+                        onOpenUnderlineConfig = onOpenUnderlineConfig,
+                        onOpenShadowSet = onOpenShadowSet,
                         onShareLayoutChange = { shareLayout ->
                             onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShareLayout(shareLayout)))
                         },
@@ -108,6 +108,7 @@ fun ReadStyleContent(
                         modifier = Modifier.padding(horizontal = 16.dp),
                         onIntent = onIntent,
                         styleConfig = styleConfig,
+                        preferences = preferences,
                     )
 
                     1 -> SystemMenuPage(
@@ -123,7 +124,7 @@ fun ReadStyleContent(
         val tabTitles = listOf(
             stringResource(R.string.read_config_global_theme),
             stringResource(R.string.read_config_menu_system),
-            stringResource(R.string.header_footer),
+            stringResource(R.string.compose_type),
             stringResource(R.string.more_setting),
         )
         CardTabRow(
@@ -139,7 +140,7 @@ fun ReadStyleContent(
                             )
                         }
                     }
-                    2 -> onOpenHeaderFooterConfig()
+                    2 -> onOpenTypographyConfig()
                     3 -> onOpenMoreConfig()
                 }
             },

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

@@ -1,734 +0,0 @@
-package io.legado.app.ui.book.read.sheet
-
-import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.animation.core.FastOutSlowInEasing
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.pager.HorizontalPager
-import androidx.compose.foundation.pager.PagerState
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.FormatItalic
-import androidx.compose.material.icons.filled.FormatUnderlined
-import androidx.compose.material.icons.filled.Layers
-import androidx.compose.material.icons.filled.TextFields
-import androidx.compose.material.icons.filled.Tune
-import androidx.compose.material3.Icon
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableFloatStateOf
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateMapOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clipToBounds
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.res.stringArrayResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import io.legado.app.R
-import io.legado.app.ui.book.read.ConfigUpdate
-import io.legado.app.ui.book.read.ReadBookIntent
-import io.legado.app.ui.book.read.ReadSheetConfigUiState
-import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.AppSlider
-import io.legado.app.ui.widget.components.AppTextField
-import io.legado.app.ui.widget.components.SectionTitle
-import io.legado.app.ui.widget.components.ValueStepper
-import io.legado.app.ui.widget.components.alert.AppAlertDialog
-import io.legado.app.ui.widget.components.card.NormalCard
-import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
-import io.legado.app.ui.widget.components.pager.pagerHeight
-import io.legado.app.ui.widget.components.pager.rememberPagerAnimatedHeight
-import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
-import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem
-import io.legado.app.ui.widget.components.settingItem.TinyColorModeSettingItem
-import io.legado.app.ui.widget.components.settingItem.TinyColorSettingItem
-import io.legado.app.ui.widget.components.settingItem.TinyDropdownSettingItem
-import io.legado.app.ui.widget.components.settingItem.TinySliderSettingItem
-import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem
-import io.legado.app.ui.widget.components.tabRow.CardTabRow
-import kotlinx.coroutines.launch
-import kotlin.math.roundToInt
-
-// Color picker IDs
-private const val COLOR_TEXT = 1
-private const val COLOR_ACCENT = 2
-private const val COLOR_TITLE = 4
-private const val COLOR_TITLE_NIGHT = 5
-
-@Composable
-fun ReadStyleTextTitleContent(
-    config: ReadSheetConfigUiState,
-    onOpenShadowSet: () -> Unit,
-    onOpenUnderlineConfig: () -> Unit,
-    onOpenHighlightRule: () -> Unit,
-    onOpenFontSelect: () -> Unit,
-    onOpenTitleFontSelect: () -> Unit,
-    modifier: Modifier = Modifier,
-    onIntent: (ReadBookIntent) -> Unit,
-) {
-    val scope = rememberCoroutineScope()
-    val tabTitles = listOf(
-        stringResource(R.string.read_config_text_effects),
-        stringResource(R.string.read_config_layout_spacing),
-        stringResource(R.string.read_config_title_settings),
-    )
-    val pagerState = rememberPagerState(pageCount = { 3 })
-    var selectedTab by remember { mutableIntStateOf(0) }
-    var clickScrollCount by remember { mutableIntStateOf(0) }
-
-    LaunchedEffect(pagerState) {
-        snapshotFlow { pagerState.currentPage }.collect { page ->
-            if (clickScrollCount == 0) {
-                selectedTab = page
-            }
-        }
-    }
-
-    ReadStyleTextTitleContent(
-        config = config,
-        tabTitles = tabTitles,
-        selectedTab = selectedTab,
-        pagerState = pagerState,
-        onOpenShadowSet = onOpenShadowSet,
-        onOpenUnderlineConfig = onOpenUnderlineConfig,
-        onOpenHighlightRule = onOpenHighlightRule,
-        onOpenFontSelect = onOpenFontSelect,
-        onOpenTitleFontSelect = onOpenTitleFontSelect,
-        animateToPage = { page ->
-            selectedTab = page
-            clickScrollCount++
-            scope.launch {
-                try {
-                    pagerState.animateScrollToPage(
-                        page = page,
-                        animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
-                    )
-                } finally {
-                    clickScrollCount = (clickScrollCount - 1).coerceAtLeast(0)
-                }
-            }
-        },
-        modifier = modifier,
-        onIntent = onIntent,
-    )
-}
-
-@Composable
-internal fun ReadStyleTextTitleContent(
-    config: ReadSheetConfigUiState,
-    tabTitles: List<String>,
-    selectedTab: Int,
-    pagerState: PagerState,
-    onOpenShadowSet: () -> Unit,
-    onOpenUnderlineConfig: () -> Unit,
-    onOpenHighlightRule: () -> Unit,
-    onOpenFontSelect: () -> Unit,
-    onOpenTitleFontSelect: () -> Unit,
-    animateToPage: (Int) -> Unit,
-    modifier: Modifier = Modifier,
-    onIntent: (ReadBookIntent) -> Unit,
-) {
-    val pageHeights = remember { mutableStateMapOf<Int, Int>() }
-    val animatedHeight by rememberPagerAnimatedHeight(pagerState, pageHeights)
-    val childPagerNestedScrollConnection = rememberPagerFlingPassThroughConnection(
-        state = pagerState,
-        orientation = Orientation.Horizontal,
-    )
-
-    Column(
-        modifier = modifier
-            .fillMaxWidth()
-    ) {
-        CardTabRow(
-            tabTitles = tabTitles,
-            selectedTabIndex = selectedTab,
-            onTabSelected = { index ->
-                animateToPage(index)
-            },
-            modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),
-        )
-        HorizontalPager(
-            state = pagerState,
-            verticalAlignment = Alignment.Top,
-            pageNestedScrollConnection = childPagerNestedScrollConnection,
-            modifier = Modifier
-                .fillMaxWidth()
-                .clipToBounds()
-                .pagerHeight(animatedHeight),
-        ) { page ->
-            Box(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .onSizeChanged { size ->
-                        pageHeights[page] = size.height
-                    }
-            ) {
-                when (page) {
-                    0 -> TextEffectsPage(
-                        config = config,
-                        onOpenShadowSet = onOpenShadowSet,
-                        onOpenUnderlineConfig = onOpenUnderlineConfig,
-                        onOpenHighlightRule = onOpenHighlightRule,
-                        onOpenFontSelect = onOpenFontSelect,
-                        onIntent = onIntent,
-                    )
-
-                    1 -> LayoutSpacingPage(config = config, onIntent = onIntent)
-                    2 -> TitleSettingsPage(
-                        config = config,
-                        onOpenTitleFontSelect = onOpenTitleFontSelect,
-                        onIntent = onIntent,
-                    )
-                }
-            }
-        }
-    }
-}
-
-// ========== Tab: Layout & Spacing ==========
-
-@Composable
-internal fun LayoutSpacingPage(
-    config: ReadSheetConfigUiState,
-    modifier: Modifier = Modifier,
-    onIntent: (ReadBookIntent) -> Unit,
-) {
-    var letterSpacing by remember { mutableFloatStateOf(config.letterSpacing) }
-    var lineSpacing by remember { mutableFloatStateOf(config.lineSpacing.toFloat()) }
-    var paragraphSpacing by remember { mutableFloatStateOf(config.paragraphSpacing.toFloat()) }
-    var indentCount by remember { mutableIntStateOf(config.paragraphIndentCount) }
-
-    Column(
-        modifier = modifier
-            .fillMaxWidth()
-            .padding(horizontal = 16.dp)
-            .verticalScroll(rememberScrollState()),
-    ) {
-        SectionTitle(stringResource(R.string.read_config_body_spacing))
-        TinySliderSettingItem(
-            title = stringResource(R.string.text_indent),
-            value = indentCount.toFloat(),
-            valueRange = 0f..4f,
-            steps = 3,
-            onValueChange = { value ->
-                indentCount = value.toInt()
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ParagraphIndent(" ".repeat(indentCount))))
-            },
-        )
-        TinySliderSettingItem(
-            title = stringResource(R.string.text_letter_spacing),
-            value = (letterSpacing * 100) + 50,
-            valueRange = 0f..100f,
-            steps = 99,
-            valueFormat = { ((it - 50) / 100f).toString() },
-            onValueChange = { value ->
-                letterSpacing = (value - 50) / 100f
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.LetterSpacing(letterSpacing)))
-            },
-        )
-        TinySliderSettingItem(
-            title = stringResource(R.string.line_size),
-            value = lineSpacing,
-            valueRange = 0f..20f,
-            steps = 19,
-            valueFormat = { ((it - 10) / 10f).toString() },
-            onValueChange = { value ->
-                lineSpacing = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.LineSpacing(value.toInt())))
-            },
-        )
-        TinySliderSettingItem(
-            title = stringResource(R.string.paragraph_size),
-            value = paragraphSpacing,
-            valueRange = 0f..20f,
-            steps = 19,
-            valueFormat = { (it / 10f).toString() },
-            onValueChange = { value ->
-                paragraphSpacing = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ParagraphSpacing(value.toInt())))
-            },
-        )
-    }
-}
-
-// ========== Text & Effects (sub-page) ==========
-
-@Composable
-internal fun TextEffectsPage(
-    config: ReadSheetConfigUiState,
-    onOpenShadowSet: () -> Unit,
-    onOpenUnderlineConfig: () -> Unit,
-    onOpenHighlightRule: () -> Unit,
-    onOpenFontSelect: () -> Unit,
-    modifier: Modifier = Modifier,
-    onIntent: (ReadBookIntent) -> Unit,
-) {
-    var textItalic by remember { mutableStateOf(config.textItalic) }
-    var textBold by remember { mutableIntStateOf(config.textBold) }
-
-    var showColorPicker by remember { mutableStateOf(false) }
-    var colorPickerId by remember { mutableIntStateOf(0) }
-    var colorPickerInitial by remember { mutableIntStateOf(0) }
-
-    Column(
-        modifier = modifier
-            .fillMaxWidth()
-            .padding(horizontal = 16.dp)
-            .verticalScroll(rememberScrollState()),
-    ) {
-        SectionTitle(stringResource(R.string.text_typeface))
-        TinySwitchSettingItem(
-            title = stringResource(R.string.read_config_italic),
-            checked = textItalic,
-            imageVector = Icons.Default.FormatItalic,
-            onCheckedChange = {
-                textItalic = it
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TextItalic(it)))
-            },
-        )
-        FontWeightSetting(
-            value = textBold,
-            onValueChange = { value ->
-                textBold = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TextBold(value)))
-            },
-        )
-
-        TinyClickableSettingItem(
-            title = stringResource(R.string.select_font),
-            imageVector = Icons.Default.TextFields,
-            onClick = onOpenFontSelect,
-        )
-
-        val chineseConvertEntries = stringArrayResource(R.array.chinese_mode)
-        val chineseConvertValues = remember { arrayOf("0", "1", "2") }
-        TinyDropdownSettingItem(
-            title = stringResource(R.string.chinese_converter),
-            selectedValue = config.chineseConverterType.toString(),
-            displayEntries = chineseConvertEntries,
-            entryValues = chineseConvertValues,
-            onValueChange = {
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ChineseConverterType(it.toInt())))
-            },
-        )
-
-        // Colors
-        SectionTitle(stringResource(R.string.read_color))
-        TinyColorSettingItem(
-            title = stringResource(R.string.text_color),
-            colorValue = config.textColor,
-            onClick = {
-                colorPickerId = COLOR_TEXT
-                colorPickerInitial = config.textColor
-                showColorPicker = true
-            },
-        )
-        TinyColorSettingItem(
-            title = stringResource(R.string.text_accent_color),
-            colorValue = config.textAccentColor,
-            onClick = {
-                colorPickerId = COLOR_ACCENT
-                colorPickerInitial = config.textAccentColor
-                showColorPicker = true
-            },
-        )
-
-        SectionTitle(stringResource(R.string.read_config_effects))
-        TinyClickableSettingItem(
-            title = stringResource(R.string.text_shadow_set),
-            description = stringResource(R.string.read_config_shadow_desc),
-            imageVector = Icons.Default.Layers,
-            onClick = onOpenShadowSet,
-        )
-        TinyClickableSettingItem(
-            title = stringResource(R.string.text_underline),
-            description = stringResource(R.string.read_config_underline_desc),
-            imageVector = Icons.Default.FormatUnderlined,
-            onClick = onOpenUnderlineConfig,
-        )
-        TinyClickableSettingItem(
-            title = stringResource(R.string.highlight_rule_config),
-            description = stringResource(R.string.read_config_regex_desc),
-            imageVector = Icons.Default.Tune,
-            onClick = onOpenHighlightRule,
-        )
-
-        Spacer(Modifier.height(8.dp))
-    }
-
-    // Color picker
-    ColorPickerSheet(
-        show = showColorPicker,
-        initialColor = colorPickerInitial,
-        onDismissRequest = { showColorPicker = false },
-        onColorSelected = { color ->
-            when (colorPickerId) {
-                COLOR_TEXT -> {
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TextColor(color)))
-                }
-
-                COLOR_ACCENT -> {
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TextAccentColor(color)))
-                }
-            }
-            showColorPicker = false
-        },
-    )
-}
-
-// ========== Title Settings (sub-page) ==========
-
-@Composable
-internal fun TitleSettingsPage(
-    config: ReadSheetConfigUiState,
-    onOpenTitleFontSelect: () -> Unit,
-    modifier: Modifier = Modifier,
-    onIntent: (ReadBookIntent) -> Unit,
-) {
-    var titleMode by remember(config.titleMode) { mutableIntStateOf(config.titleMode) }
-    var titleBold by remember(config.titleBold) { mutableIntStateOf(config.titleBold) }
-    var titleSegType by remember(config.titleSegType) { mutableIntStateOf(config.titleSegType) }
-    var titleSegDistance by remember(config.titleSegDistance) { mutableIntStateOf(config.titleSegDistance) }
-    var titleSegFlag by remember(config.titleSegFlag) { mutableStateOf(config.titleSegFlag) }
-    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 titleSize by remember(config.titleSize) { mutableIntStateOf(config.titleSize) }
-    var titleTopSpacing by remember(config.titleTopSpacing) { mutableIntStateOf(config.titleTopSpacing) }
-    var titleBottomSpacing by remember(config.titleBottomSpacing) { mutableIntStateOf(config.titleBottomSpacing) }
-
-    var showColorPicker by remember { mutableStateOf(false) }
-    var colorPickerId by remember { mutableIntStateOf(0) }
-    var colorPickerInitial by remember { mutableIntStateOf(0) }
-
-    Column(
-        modifier = modifier
-            .fillMaxWidth()
-            .padding(horizontal = 16.dp)
-            .verticalScroll(rememberScrollState()),
-    ) {
-        TinyDropdownSettingItem(
-            title = stringResource(R.string.body_title),
-            selectedValue = titleMode.toString(),
-            displayEntries = arrayOf(
-                stringResource(R.string.title_left),
-                stringResource(R.string.title_center),
-                stringResource(R.string.title_hide),
-            ),
-            entryValues = arrayOf("0", "1", "2"),
-            onValueChange = {
-                titleMode = it.toInt()
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleMode(titleMode)))
-            },
-        )
-
-        FontWeightSetting(
-            value = titleBold,
-            onValueChange = { value ->
-                titleBold = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleBold(value)))
-            },
-        )
-
-        TinyColorModeSettingItem(
-            title = stringResource(R.string.title_color),
-            dayColor = if (config.titleColor != 0) {
-                config.titleColor
-            } else {
-                config.textColorDay
-            },
-            nightColor = if (config.titleColorNight != 0) {
-                config.titleColorNight
-            } else {
-                config.textColorNight
-            },
-            onClickColor = { isNight ->
-                if (isNight) {
-                    colorPickerId = COLOR_TITLE_NIGHT
-                    colorPickerInitial = if (config.titleColorNight != 0) {
-                        config.titleColorNight
-                    } else {
-                        config.textColorNight
-                    }
-                } else {
-                    colorPickerId = COLOR_TITLE
-                    colorPickerInitial = if (config.titleColor != 0) {
-                        config.titleColor
-                    } else {
-                        config.textColorDay
-                    }
-                }
-                showColorPicker = true
-            },
-        )
-
-        // Title font
-        TinyClickableSettingItem(
-            title = stringResource(R.string.read_config_title_settings),
-            imageVector = Icons.Default.TextFields,
-            onClick = onOpenTitleFontSelect,
-        )
-
-        // Title segmentation
-        TinyDropdownSettingItem(
-            title = stringResource(R.string.split_title_mode),
-            selectedValue = titleSegType.toString(),
-            displayEntries = arrayOf(
-                stringResource(R.string.close),
-                stringResource(R.string.split_title_by_position),
-                stringResource(R.string.split_title_by_flag),
-                stringResource(R.string.split_title_by_regex),
-            ),
-            entryValues = arrayOf("0", "1", "2", "3"),
-            onValueChange = {
-                titleSegType = it.toInt()
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleSegType(titleSegType)))
-            },
-        )
-        if (titleSegType == 1) {
-            TinySliderSettingItem(
-                title = stringResource(R.string.split_title_position),
-                value = titleSegDistance.toFloat(),
-                valueRange = 1f..20f,
-                steps = 18,
-                onValueChange = { value ->
-                    titleSegDistance = value.toInt()
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleSegDistance(titleSegDistance)))
-                },
-            )
-        }
-        if (titleSegType == 2 || titleSegType == 3) {
-            var showFlagDialog by remember { mutableStateOf(false) }
-            var flagText by remember { mutableStateOf(titleSegFlag) }
-
-            TinyClickableSettingItem(
-                title = stringResource(R.string.rule_segment),
-                description = titleSegFlag.ifBlank { stringResource(R.string.split_title_mode) },
-                onClick = { showFlagDialog = true },
-            )
-
-            AppAlertDialog(
-                show = showFlagDialog,
-                onDismissRequest = { showFlagDialog = false },
-                title = stringResource(R.string.rule_segment),
-                content = {
-                    AppTextField(
-                        value = flagText,
-                        onValueChange = { flagText = it },
-                        singleLine = false,
-                        modifier = Modifier.fillMaxWidth(),
-                    )
-                },
-                confirmText = stringResource(android.R.string.ok),
-                onConfirm = {
-                    titleSegFlag = flagText
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleSegFlag(titleSegFlag)))
-                    showFlagDialog = false
-                },
-                dismissText = stringResource(android.R.string.cancel),
-                onDismiss = { showFlagDialog = false },
-            )
-        }
-
-        // Title spacing sliders
-        TinySliderSettingItem(
-            title = stringResource(R.string.subtitle_scale),
-            value = titleSegScaling,
-            valueRange = 0f..2f,
-            steps = 19,
-            stepSize = 0.1f,
-            valueFormat = { "%.1f".format(it) },
-            onValueChange = { value ->
-                titleSegScaling = (value * 10).roundToInt() / 10f
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleSegScaling(titleSegScaling)))
-            },
-        )
-        TinySliderSettingItem(
-            title = stringResource(R.string.heading_spacing),
-            value = titleLineSpacingExtra.toFloat(),
-            valueRange = 0f..20f,
-            onValueChange = { value ->
-                titleLineSpacingExtra = value.toInt()
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleLineSpacingExtra(titleLineSpacingExtra)))
-            },
-        )
-        TinySliderSettingItem(
-            title = stringResource(R.string.subtitle_margin),
-            value = titleLineSpacingSub.toFloat(),
-            valueRange = -30f..30f,
-            onValueChange = { value ->
-                titleLineSpacingSub = value.toInt()
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleLineSpacingSub(titleLineSpacingSub)))
-            },
-        )
-        TinySliderSettingItem(
-            title = stringResource(R.string.title_font_size),
-            value = titleSize.toFloat(),
-            valueRange = 0f..20f,
-            onValueChange = { value ->
-                titleSize = value.toInt()
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleSize(titleSize)))
-            },
-        )
-        TinySliderSettingItem(
-            title = stringResource(R.string.title_margin_top),
-            value = titleTopSpacing.toFloat(),
-            valueRange = 0f..200f,
-            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,
-            onValueChange = { value ->
-                titleBottomSpacing = value.toInt()
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleBottomSpacing(titleBottomSpacing)))
-            },
-        )
-
-    }
-
-    // Color picker
-    ColorPickerSheet(
-        show = showColorPicker,
-        initialColor = colorPickerInitial,
-        onDismissRequest = { showColorPicker = false },
-        onColorSelected = { color ->
-            when (colorPickerId) {
-                COLOR_TITLE -> {
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleColor(color)))
-                }
-                COLOR_TITLE_NIGHT -> {
-                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleColorNight(color)))
-                }
-            }
-            showColorPicker = false
-        },
-    )
-}
-
-@Composable
-private fun FontWeightSetting(
-    value: Int,
-    onValueChange: (Int) -> Unit,
-) {
-    var showVariableWeight by remember { mutableStateOf(false) }
-    var sliderValue by remember(value) {
-        mutableFloatStateOf(
-            when (value) {
-                2 -> 300f
-                0 -> 400f
-                1 -> 900f
-                else -> value.coerceIn(100, 900).toFloat()
-            }
-        )
-    }
-    val weightEntries = stringArrayResource(R.array.text_font_weight)
-
-    Column {
-        Row(
-            modifier = Modifier.fillMaxWidth(),
-            horizontalArrangement = Arrangement.spacedBy(8.dp),
-        ) {
-            Box(modifier = Modifier.weight(1f)) {
-                TinyDropdownSettingItem(
-                    title = stringResource(R.string.font_weight_text),
-                    selectedValue = value.toString(),
-                    displayEntries = arrayOf(
-                        weightEntries[2],
-                        weightEntries[0],
-                        weightEntries[1],
-                    ),
-                    entryValues = arrayOf("2", "0", "1"),
-                    onValueChange = { onValueChange(it.toInt()) },
-                )
-            }
-            NormalCard(
-                onClick = { showVariableWeight = !showVariableWeight },
-                modifier = Modifier
-                    .padding(bottom = 4.dp)
-                    .height(56.dp)
-                    .aspectRatio(1f),
-                containerColor = if (showVariableWeight) {
-                    LegadoTheme.colorScheme.secondaryContainer
-                } else {
-                    LegadoTheme.colorScheme.surfaceContainerLow
-                },
-                contentColor = if (showVariableWeight) {
-                    LegadoTheme.colorScheme.onSecondaryContainer
-                } else {
-                    LegadoTheme.colorScheme.onSurfaceVariant
-                },
-                cornerRadius = 12.dp,
-            ) {
-                Box(
-                    modifier = Modifier.fillMaxSize(),
-                    contentAlignment = Alignment.Center,
-                ) {
-                    Icon(
-                        imageVector = Icons.Default.Tune,
-                        contentDescription = stringResource(R.string.font_weight_text),
-                    )
-                }
-            }
-        }
-
-        AnimatedVisibility(visible = showVariableWeight) {
-            NormalCard(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .padding(bottom = 4.dp)
-                    .height(56.dp),
-                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
-                cornerRadius = 12.dp,
-            ) {
-                ValueStepper(
-                    value = sliderValue,
-                    displayValue = sliderValue,
-                    valueRange = 100f..900f,
-                    onValueChange = {
-                        sliderValue = it
-                        onValueChange(it.toInt())
-                    },
-                    modifier = Modifier
-                        .fillMaxSize()
-                        .padding(horizontal = 12.dp),
-                    content = {
-                        AppSlider(
-                            value = sliderValue,
-                            onValueChange = { sliderValue = it },
-                            onValueChangeFinished = {
-                                onValueChange(sliderValue.toInt())
-                            },
-                            valueRange = 100f..900f,
-                            modifier = Modifier.weight(1f),
-                        )
-                    },
-                )
-            }
-        }
-    }
-}

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

@@ -0,0 +1,313 @@
+package io.legado.app.ui.book.read.sheet
+
+import androidx.compose.animation.core.FastOutSlowInEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.gestures.Orientation
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateMapOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import io.legado.app.R
+import io.legado.app.help.config.ReadBookConfig
+import io.legado.app.ui.book.read.ConfigUpdate
+import io.legado.app.ui.book.read.ReadBookIntent
+import io.legado.app.ui.book.read.ReadSheetConfigUiState
+import io.legado.app.ui.widget.components.pager.pagerHeight
+import io.legado.app.ui.widget.components.pager.rememberPagerAnimatedHeight
+import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
+import io.legado.app.ui.widget.components.tabRow.CardTabRow
+import kotlinx.coroutines.launch
+
+/**
+ * 排版配置页 — 统一的 5 Tab 排版设置入口。
+ *
+ * Tabs: 正文 / 标题 / 页眉 / 页脚 / 边距
+ *
+ * 模态弹窗(FontSelectSheet / ColorPickerSheet / CustomTipDialog)状态上抛至本层级,
+ * 在 HorizontalPager 之外渲染,避免 clipToBounds 裁剪问题。
+ */
+@Composable
+fun TypographyPage(
+    config: ReadSheetConfigUiState,
+    onIntent: (ReadBookIntent) -> Unit,
+    onOpenFontSelect: () -> Unit,
+    onOpenTitleFontSelect: () -> Unit,
+    onOpenShadowSet: () -> Unit,
+    onOpenUnderlineConfig: () -> Unit,
+    onOpenHighlightRule: () -> Unit,
+    sameTitleRemoved: Boolean = false,
+    modifier: Modifier = Modifier,
+) {
+    val scope = rememberCoroutineScope()
+    val tabTitles = listOf(
+        stringResource(R.string.main_body),   // 正文
+        stringResource(R.string.body_title),   // 标题
+        stringResource(R.string.header),       // 页眉
+        stringResource(R.string.footer),       // 页脚
+        stringResource(R.string.padding),      // 边距
+    )
+    val pagerState = rememberPagerState(pageCount = { 5 })
+    var selectedTab by remember { mutableIntStateOf(0) }
+
+    // Hoisted modal sheet state — rendered outside the pager to avoid clipToBounds clipping
+    var activeColorPicker by remember { mutableStateOf<TypographyColorTarget?>(null) }
+    var activeHeaderFontSelect by remember { mutableStateOf(false) }
+    var activeFooterFontSelect by remember { mutableStateOf(false) }
+    var activeCustomTip by remember { mutableStateOf<CustomTipTarget?>(null) }
+
+    // Lifted tip state — shared between Header Tab and Footer Tab for cross-tab clearRepeat.
+    // Keys ensure re-initialization when ReadBookConfig changes from external sources.
+    var headerMode by remember(ReadBookConfig.headerMode) { mutableIntStateOf(ReadBookConfig.headerMode) }
+    var headerLeft by remember(ReadBookConfig.tipHeaderLeft) { mutableIntStateOf(ReadBookConfig.tipHeaderLeft) }
+    var headerMiddle by remember(ReadBookConfig.tipHeaderMiddle) { mutableIntStateOf(ReadBookConfig.tipHeaderMiddle) }
+    var headerRight by remember(ReadBookConfig.tipHeaderRight) { mutableIntStateOf(ReadBookConfig.tipHeaderRight) }
+    var footerMode by remember(ReadBookConfig.footerMode) { mutableIntStateOf(ReadBookConfig.footerMode) }
+    var footerLeft by remember(ReadBookConfig.tipFooterLeft) { mutableIntStateOf(ReadBookConfig.tipFooterLeft) }
+    var footerMiddle by remember(ReadBookConfig.tipFooterMiddle) { mutableIntStateOf(ReadBookConfig.tipFooterMiddle) }
+    var footerRight by remember(ReadBookConfig.tipFooterRight) { mutableIntStateOf(ReadBookConfig.tipFooterRight) }
+    var showHeaderLine by remember(ReadBookConfig.showHeaderLine) { mutableStateOf(ReadBookConfig.showHeaderLine) }
+    var showFooterLine by remember(ReadBookConfig.showFooterLine) { mutableStateOf(ReadBookConfig.showFooterLine) }
+
+    fun clearRepeat(repeat: Int) {
+        if (repeat == ReadBookConfig.tipNone) return
+        if (headerLeft == repeat) {
+            headerLeft = ReadBookConfig.tipNone
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderLeft(ReadBookConfig.tipNone)))
+        }
+        if (headerMiddle == repeat) {
+            headerMiddle = ReadBookConfig.tipNone
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderMiddle(ReadBookConfig.tipNone)))
+        }
+        if (headerRight == repeat) {
+            headerRight = ReadBookConfig.tipNone
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderRight(ReadBookConfig.tipNone)))
+        }
+        if (footerLeft == repeat) {
+            footerLeft = ReadBookConfig.tipNone
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterLeft(ReadBookConfig.tipNone)))
+        }
+        if (footerMiddle == repeat) {
+            footerMiddle = ReadBookConfig.tipNone
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterMiddle(ReadBookConfig.tipNone)))
+        }
+        if (footerRight == repeat) {
+            footerRight = ReadBookConfig.tipNone
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterRight(ReadBookConfig.tipNone)))
+        }
+    }
+
+    fun handleTipChange(target: CustomTipTarget, value: Int) {
+        if (value == ReadBookConfig.tipCustom) {
+            activeCustomTip = target
+            return
+        }
+        clearRepeat(value)
+        when (target) {
+            CustomTipTarget.HEADER_LEFT -> {
+                headerLeft = value
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderLeft(value)))
+            }
+            CustomTipTarget.HEADER_MIDDLE -> {
+                headerMiddle = value
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderMiddle(value)))
+            }
+            CustomTipTarget.HEADER_RIGHT -> {
+                headerRight = value
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderRight(value)))
+            }
+            CustomTipTarget.FOOTER_LEFT -> {
+                footerLeft = value
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterLeft(value)))
+            }
+            CustomTipTarget.FOOTER_MIDDLE -> {
+                footerMiddle = value
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterMiddle(value)))
+            }
+            CustomTipTarget.FOOTER_RIGHT -> {
+                footerRight = value
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterRight(value)))
+            }
+        }
+    }
+
+    val childPagerNestedScrollConnection = rememberPagerFlingPassThroughConnection(
+        state = pagerState,
+        orientation = Orientation.Horizontal,
+    )
+
+    val pageHeights = remember { mutableStateMapOf<Int, Int>() }
+    val animatedHeight by rememberPagerAnimatedHeight(pagerState, pageHeights)
+
+    LaunchedEffect(pagerState) {
+        snapshotFlow { pagerState.currentPage }.collect { page ->
+            selectedTab = page
+        }
+    }
+
+    Column(
+        modifier = modifier.fillMaxWidth(),
+    ) {
+        CardTabRow(
+            tabTitles = tabTitles,
+            selectedTabIndex = selectedTab,
+            onTabSelected = { index ->
+                scope.launch {
+                    pagerState.animateScrollToPage(
+                        page = index,
+                        animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
+                    )
+                }
+            },
+            modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp),
+        )
+
+        HorizontalPager(
+            state = pagerState,
+            verticalAlignment = Alignment.Top,
+            pageNestedScrollConnection = childPagerNestedScrollConnection,
+            modifier = Modifier
+                .weight(1f, fill = false)
+                .clipToBounds()
+                .pagerHeight(animatedHeight),
+        ) { page ->
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .onSizeChanged { size ->
+                        pageHeights[page] = size.height
+                    }
+            ) {
+                when (page) {
+                    0 -> TypographyBodyTab(
+                        config = config,
+                        onIntent = onIntent,
+                        onOpenFontSelect = onOpenFontSelect,
+                        onOpenShadowSet = onOpenShadowSet,
+                        onOpenUnderlineConfig = onOpenUnderlineConfig,
+                        onOpenHighlightRule = onOpenHighlightRule,
+                        onOpenColorPicker = { activeColorPicker = it },
+                        modifier = Modifier.padding(horizontal = 16.dp),
+                    )
+
+                    1 -> TypographyTitleTab(
+                        config = config,
+                        onIntent = onIntent,
+                        onOpenTitleFontSelect = onOpenTitleFontSelect,
+                        onOpenColorPicker = { activeColorPicker = it },
+                        sameTitleRemoved = sameTitleRemoved,
+                        modifier = Modifier.padding(horizontal = 16.dp),
+                    )
+
+                    2 -> TypographyHeaderTab(
+                        config = config,
+                        onIntent = onIntent,
+                        onOpenHeaderFontSelect = { activeHeaderFontSelect = true },
+                        onOpenColorPicker = { activeColorPicker = it },
+                        onOpenCustomTip = { target: CustomTipTarget -> activeCustomTip = target },
+                        headerMode = headerMode,
+                        headerLeft = headerLeft,
+                        headerMiddle = headerMiddle,
+                        headerRight = headerRight,
+                        onHeaderModeChange = {
+                            headerMode = it
+                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderMode(it)))
+                        },
+                        onTipChange = ::handleTipChange,
+                        showHeaderLine = showHeaderLine,
+                        onShowHeaderLineChange = {
+                            showHeaderLine = it
+                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShowHeaderLine(it)))
+                        },
+                        modifier = Modifier.padding(horizontal = 16.dp),
+                    )
+
+                    3 -> TypographyFooterTab(
+                        config = config,
+                        onIntent = onIntent,
+                        onOpenFooterFontSelect = { activeFooterFontSelect = true },
+                        onOpenColorPicker = { activeColorPicker = it },
+                        onOpenCustomTip = { target: CustomTipTarget -> activeCustomTip = target },
+                        footerMode = footerMode,
+                        footerLeft = footerLeft,
+                        footerMiddle = footerMiddle,
+                        footerRight = footerRight,
+                        onFooterModeChange = {
+                            footerMode = it
+                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterMode(it)))
+                        },
+                        onTipChange = ::handleTipChange,
+                        showFooterLine = showFooterLine,
+                        onShowFooterLineChange = {
+                            showFooterLine = it
+                            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShowFooterLine(it)))
+                        },
+                        modifier = Modifier.padding(horizontal = 16.dp),
+                    )
+
+                    4 -> TypographyMarginTab(
+                        config = config,
+                        onIntent = onIntent,
+                        modifier = Modifier.padding(horizontal = 16.dp),
+                    )
+                }
+            }
+        }
+    }
+
+    // --- Hoisted modal sheets (rendered outside the pager) ---
+
+    // Color picker
+    activeColorPicker?.let { target ->
+        TypographyColorPickerSheet(
+            target = target,
+            config = config,
+            onDismiss = { activeColorPicker = null },
+            onIntent = onIntent,
+        )
+    }
+
+    // Header font select
+    if (activeHeaderFontSelect) {
+        TypographyHeaderFontSelectSheet(
+            config = config,
+            onDismiss = { activeHeaderFontSelect = false },
+            onIntent = onIntent,
+        )
+    }
+
+    // Footer font select
+    if (activeFooterFontSelect) {
+        TypographyFooterFontSelectSheet(
+            config = config,
+            onDismiss = { activeFooterFontSelect = false },
+            onIntent = onIntent,
+        )
+    }
+
+    // Custom tip dialog
+    activeCustomTip?.let { target ->
+        TypographyCustomTipDialog(
+            target = target,
+            onDismiss = { activeCustomTip = null },
+            onIntent = onIntent,
+        )
+    }
+}

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

@@ -0,0 +1,1066 @@
+package io.legado.app.ui.book.read.sheet
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.CleanHands
+import androidx.compose.material.icons.filled.FormatItalic
+import androidx.compose.material.icons.filled.FormatUnderlined
+import androidx.compose.material.icons.filled.Image
+import androidx.compose.material.icons.filled.Layers
+import androidx.compose.material.icons.filled.TextFields
+import androidx.compose.material.icons.filled.Tune
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringArrayResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.core.net.toUri
+import io.legado.app.R
+import io.legado.app.data.entities.Book
+import io.legado.app.data.repository.ReadSettingsRepository
+import io.legado.app.help.config.ReadBookConfig
+import io.legado.app.model.ReadBook
+import io.legado.app.ui.book.read.ConfigUpdate
+import io.legado.app.ui.book.read.ReadBookIntent
+import io.legado.app.ui.book.read.ReadSheetConfigUiState
+import io.legado.app.ui.widget.components.AppTextField
+import io.legado.app.ui.widget.components.FontFolderState
+import io.legado.app.ui.widget.components.FontSelectSheet
+import io.legado.app.ui.widget.components.SectionTitle
+import io.legado.app.ui.widget.components.alert.AppAlertDialog
+import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
+import io.legado.app.ui.widget.components.dialog.CustomTipDialog
+import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem
+import io.legado.app.ui.widget.components.settingItem.TinyColorModeSettingItem
+import io.legado.app.ui.widget.components.settingItem.TinyColorSettingItem
+import io.legado.app.ui.widget.components.settingItem.TinyDropdownSettingItem
+import io.legado.app.ui.widget.components.settingItem.TinySliderSettingItem
+import io.legado.app.ui.widget.components.settingItem.TinySwitchSettingItem
+import io.legado.app.utils.getCompatColor
+import org.koin.compose.koinInject
+import kotlin.math.roundToInt
+
+// region Modal target sealed interface
+
+/** Identifies which color picker to show in the typography page. */
+internal sealed interface TypographyColorTarget {
+    data object Text : TypographyColorTarget
+    data object TextAccent : TypographyColorTarget
+    data object Title : TypographyColorTarget
+    data object TitleNight : TypographyColorTarget
+    data object Header : TypographyColorTarget
+    data object HeaderNight : TypographyColorTarget
+    data object Footer : TypographyColorTarget
+    data object FooterNight : TypographyColorTarget
+    data object Divider : TypographyColorTarget
+}
+
+// endregion
+
+// region Dropdown & Slider helpers
+
+@Composable
+private fun FontWeightSetting(
+    value: Int,
+    onValueChange: (Int) -> Unit,
+) {
+    val weightEntries = stringArrayResource(R.array.text_font_weight)
+    val weightValues = remember { arrayOf("0", "1", "2") }
+    TinyDropdownSettingItem(
+        title = stringResource(R.string.font_weight_text),
+        selectedValue = value.toString(),
+        displayEntries = weightEntries,
+        entryValues = weightValues,
+        onValueChange = { onValueChange(it.toInt()) },
+    )
+}
+
+@Composable
+private fun TipPositionDropdown(
+    label: String,
+    value: Int,
+    tipNames: List<String>,
+    tipValues: Array<Int>,
+    onValueChange: (Int) -> Unit,
+) {
+    TinyDropdownSettingItem(
+        title = label,
+        selectedValue = value.toString(),
+        displayEntries = tipNames.toTypedArray(),
+        entryValues = tipValues.map { it.toString() }.toTypedArray(),
+        onValueChange = { onValueChange(it.toInt()) },
+    )
+}
+
+@Composable
+private fun PaddingSliders(
+    top: Float,
+    bottom: Float,
+    left: Float,
+    right: Float,
+    onTopChange: (Float) -> Unit,
+    onBottomChange: (Float) -> Unit,
+    onLeftChange: (Float) -> Unit,
+    onRightChange: (Float) -> Unit,
+) {
+    TinySliderSettingItem(
+        title = stringResource(R.string.padding_top),
+        value = top,
+        valueRange = 0f..200f,
+        onValueChange = onTopChange,
+    )
+    TinySliderSettingItem(
+        title = stringResource(R.string.padding_bottom),
+        value = bottom,
+        valueRange = 0f..200f,
+        onValueChange = onBottomChange,
+    )
+    TinySliderSettingItem(
+        title = stringResource(R.string.padding_left),
+        value = left,
+        valueRange = 0f..200f,
+        onValueChange = onLeftChange,
+    )
+    TinySliderSettingItem(
+        title = stringResource(R.string.padding_right),
+        value = right,
+        valueRange = 0f..200f,
+        onValueChange = onRightChange,
+    )
+}
+
+// endregion
+
+// region Tab composables (placeholders — content implemented in subsequent tasks)
+
+@Composable
+internal fun TypographyBodyTab(
+    config: ReadSheetConfigUiState,
+    onIntent: (ReadBookIntent) -> Unit,
+    onOpenFontSelect: () -> Unit,
+    onOpenShadowSet: () -> Unit,
+    onOpenUnderlineConfig: () -> Unit,
+    onOpenHighlightRule: () -> Unit,
+    onOpenColorPicker: (TypographyColorTarget) -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    var textItalic by remember(config.textItalic) { mutableStateOf(config.textItalic) }
+    var textBold by remember(config.textBold) { mutableIntStateOf(config.textBold) }
+    var letterSpacing by remember(config.letterSpacing) { mutableFloatStateOf(config.letterSpacing) }
+    var lineSpacing by remember(config.lineSpacing) { mutableFloatStateOf(config.lineSpacing.toFloat()) }
+    var paragraphSpacing by remember(config.paragraphSpacing) { mutableFloatStateOf(config.paragraphSpacing.toFloat()) }
+    var indentCount by remember(config.paragraphIndentCount) { mutableIntStateOf(config.paragraphIndentCount) }
+
+    Column(
+        modifier = modifier
+            .fillMaxWidth()
+            .verticalScroll(rememberScrollState()),
+    ) {
+        // 字体组
+        SectionTitle(title = stringResource(R.string.text_typeface))
+        TinyClickableSettingItem(
+            title = stringResource(R.string.select_font),
+            imageVector = Icons.Default.TextFields,
+            onClick = onOpenFontSelect,
+        )
+        TinySwitchSettingItem(
+            title = stringResource(R.string.read_config_italic),
+            checked = textItalic,
+            imageVector = Icons.Default.FormatItalic,
+            onCheckedChange = {
+                textItalic = it
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TextItalic(it)))
+            },
+        )
+        FontWeightSetting(
+            value = textBold,
+            onValueChange = { value ->
+                textBold = value
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TextBold(value)))
+            },
+        )
+        val chineseConvertEntries = stringArrayResource(R.array.chinese_mode)
+        val chineseConvertValues = remember { arrayOf("0", "1", "2") }
+        TinyDropdownSettingItem(
+            title = stringResource(R.string.chinese_converter),
+            selectedValue = config.chineseConverterType.toString(),
+            displayEntries = chineseConvertEntries,
+            entryValues = chineseConvertValues,
+            onValueChange = {
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ChineseConverterType(it.toInt())))
+            },
+        )
+
+        // 颜色组
+        SectionTitle(title = stringResource(R.string.read_color))
+        TinyColorSettingItem(
+            title = stringResource(R.string.text_color),
+            colorValue = config.textColor,
+            onClick = { onOpenColorPicker(TypographyColorTarget.Text) },
+        )
+        TinyColorSettingItem(
+            title = stringResource(R.string.text_accent_color),
+            colorValue = config.textAccentColor,
+            onClick = { onOpenColorPicker(TypographyColorTarget.TextAccent) },
+        )
+
+        // 效果组
+        SectionTitle(title = stringResource(R.string.read_config_effects))
+        TinyClickableSettingItem(
+            title = stringResource(R.string.text_shadow_set),
+            description = stringResource(R.string.read_config_shadow_desc),
+            imageVector = Icons.Default.Layers,
+            onClick = onOpenShadowSet,
+        )
+        TinyClickableSettingItem(
+            title = stringResource(R.string.text_underline),
+            description = stringResource(R.string.read_config_underline_desc),
+            imageVector = Icons.Default.FormatUnderlined,
+            onClick = onOpenUnderlineConfig,
+        )
+        TinyClickableSettingItem(
+            title = stringResource(R.string.highlight_rule_config),
+            description = stringResource(R.string.read_config_regex_desc),
+            imageVector = Icons.Default.Tune,
+            onClick = onOpenHighlightRule,
+        )
+
+        // 间距组
+        SectionTitle(title = stringResource(R.string.read_config_body_spacing))
+        TinySliderSettingItem(
+            title = stringResource(R.string.text_indent),
+            value = indentCount.toFloat(),
+            valueRange = 0f..4f,
+            steps = 3,
+            onValueChange = { value ->
+                indentCount = value.toInt()
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ParagraphIndent(" ".repeat(indentCount))))
+            },
+        )
+        TinySliderSettingItem(
+            title = stringResource(R.string.text_letter_spacing),
+            value = (letterSpacing * 100) + 50,
+            valueRange = 0f..100f,
+            steps = 99,
+            valueFormat = { ((it - 50) / 100f).toString() },
+            onValueChange = { value ->
+                letterSpacing = (value - 50) / 100f
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.LetterSpacing(letterSpacing)))
+            },
+        )
+        TinySliderSettingItem(
+            title = stringResource(R.string.line_size),
+            value = lineSpacing,
+            valueRange = 0f..20f,
+            steps = 19,
+            valueFormat = { ((it - 10) / 10f).toString() },
+            onValueChange = { value ->
+                lineSpacing = value
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.LineSpacing(value.toInt())))
+            },
+        )
+        TinySliderSettingItem(
+            title = stringResource(R.string.paragraph_size),
+            value = paragraphSpacing,
+            valueRange = 0f..20f,
+            steps = 19,
+            valueFormat = { (it / 10f).toString() },
+            onValueChange = { value ->
+                paragraphSpacing = value
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ParagraphSpacing(value.toInt())))
+            },
+        )
+
+        // 对齐组
+        SectionTitle(title = stringResource(R.string.text_alignment))
+        TinySwitchSettingItem(
+            title = stringResource(R.string.text_full_justify),
+            checked = config.textFullJustify,
+            onCheckedChange = {
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TextFullJustify(it)))
+            },
+        )
+        TinySwitchSettingItem(
+            title = stringResource(R.string.text_bottom_justify),
+            checked = config.textBottomJustify,
+            onCheckedChange = {
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TextBottomJustify(it)))
+            },
+        )
+
+        // 图片组
+        SectionTitle(title = stringResource(R.string.image_style))
+        val imgStyleEntries = arrayOf(
+            stringResource(R.string.btn_default_s),
+            stringResource(R.string.image_style_full),
+            stringResource(R.string.image_style_text),
+            stringResource(R.string.image_style_single),
+        )
+        val imgStyleValues = arrayOf(
+            Book.imgStyleDefault,
+            Book.imgStyleFull,
+            Book.imgStyleText,
+            Book.imgStyleSingle,
+        )
+        TinyDropdownSettingItem(
+            title = stringResource(R.string.image_style),
+            selectedValue = ReadBook.book?.getImageStyle() ?: Book.imgStyleDefault,
+            displayEntries = imgStyleEntries,
+            entryValues = imgStyleValues,
+            onValueChange = { style ->
+                onIntent(ReadBookIntent.MenuImageStyle(style))
+            },
+        )
+
+        Spacer(Modifier.height(8.dp))
+    }
+}
+
+@Composable
+internal fun TypographyTitleTab(
+    config: ReadSheetConfigUiState,
+    onIntent: (ReadBookIntent) -> Unit,
+    onOpenTitleFontSelect: () -> Unit,
+    onOpenColorPicker: (TypographyColorTarget) -> Unit,
+    sameTitleRemoved: Boolean = false,
+    modifier: Modifier = Modifier,
+) {
+    var titleMode by remember(config.titleMode) { mutableIntStateOf(config.titleMode) }
+    var titleBold by remember(config.titleBold) { mutableIntStateOf(config.titleBold) }
+    var titleSegType by remember(config.titleSegType) { mutableIntStateOf(config.titleSegType) }
+    var titleSegDistance by remember(config.titleSegDistance) { mutableIntStateOf(config.titleSegDistance) }
+    var titleSegFlag by remember(config.titleSegFlag) { mutableStateOf(config.titleSegFlag) }
+    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) }
+
+    // Title font size: <8 means relative offset (textSize + titleSize), >=8 means absolute
+    var titleSize by remember(config.titleSize) {
+        val initial = if (config.titleSize < 8) {
+            (ReadBookConfig.textSize + config.titleSize).coerceIn(8, 60)
+        } else {
+            config.titleSize
+        }
+        mutableIntStateOf(initial)
+    }
+
+    var showFlagDialog by remember { mutableStateOf(false) }
+    var flagText by remember { mutableStateOf(titleSegFlag) }
+
+    Column(
+        modifier = modifier
+            .fillMaxWidth()
+            .verticalScroll(rememberScrollState()),
+    ) {
+        // 内容组
+        SectionTitle(title = stringResource(R.string.content))
+        TinyDropdownSettingItem(
+            title = stringResource(R.string.body_title),
+            selectedValue = titleMode.toString(),
+            displayEntries = arrayOf(
+                stringResource(R.string.title_left),
+                stringResource(R.string.title_center),
+                stringResource(R.string.title_hide),
+            ),
+            entryValues = arrayOf("0", "1", "2"),
+            onValueChange = {
+                titleMode = it.toInt()
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleMode(titleMode)))
+            },
+        )
+
+        // 字体组
+        SectionTitle(title = stringResource(R.string.text_font))
+        TinyClickableSettingItem(
+            title = stringResource(R.string.select_font),
+            imageVector = Icons.Default.TextFields,
+            onClick = onOpenTitleFontSelect,
+        )
+        FontWeightSetting(
+            value = titleBold,
+            onValueChange = { value ->
+                titleBold = value
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleBold(value)))
+            },
+        )
+        TinyColorModeSettingItem(
+            title = stringResource(R.string.title_color),
+            dayColor = if (config.titleColor != 0) config.titleColor else config.textColorDay,
+            nightColor = if (config.titleColorNight != 0) config.titleColorNight else config.textColorNight,
+            onClickColor = { isNight ->
+                if (isNight) {
+                    onOpenColorPicker(TypographyColorTarget.TitleNight)
+                } else {
+                    onOpenColorPicker(TypographyColorTarget.Title)
+                }
+            },
+        )
+        TinySliderSettingItem(
+            title = stringResource(R.string.title_font_size),
+            value = titleSize.toFloat(),
+            valueRange = 8f..60f,
+            steps = 51,
+            valueFormat = { "${it.toInt()}sp" },
+            onValueChange = { value ->
+                titleSize = value.toInt()
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleSize(titleSize)))
+            },
+        )
+
+        // 分段组
+        SectionTitle(title = stringResource(R.string.title_segment))
+        TinyDropdownSettingItem(
+            title = stringResource(R.string.split_title_mode),
+            selectedValue = titleSegType.toString(),
+            displayEntries = arrayOf(
+                stringResource(R.string.close),
+                stringResource(R.string.split_title_by_position),
+                stringResource(R.string.split_title_by_flag),
+                stringResource(R.string.split_title_by_regex),
+            ),
+            entryValues = arrayOf("0", "1", "2", "3"),
+            onValueChange = {
+                titleSegType = it.toInt()
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleSegType(titleSegType)))
+            },
+        )
+        if (titleSegType == 1) {
+            TinySliderSettingItem(
+                title = stringResource(R.string.split_title_position),
+                value = titleSegDistance.toFloat(),
+                valueRange = 1f..20f,
+                steps = 18,
+                onValueChange = { value ->
+                    titleSegDistance = value.toInt()
+                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleSegDistance(titleSegDistance)))
+                },
+            )
+        }
+        if (titleSegType == 2 || titleSegType == 3) {
+            TinyClickableSettingItem(
+                title = stringResource(R.string.rule_segment),
+                description = titleSegFlag.ifBlank { stringResource(R.string.split_title_mode) },
+                onClick = { showFlagDialog = true },
+            )
+        }
+        TinySliderSettingItem(
+            title = stringResource(R.string.subtitle_scale),
+            value = titleSegScaling,
+            valueRange = 0f..2f,
+            steps = 19,
+            stepSize = 0.1f,
+            valueFormat = { "%.1f".format(it) },
+            onValueChange = { value ->
+                titleSegScaling = (value * 10).roundToInt() / 10f
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleSegScaling(titleSegScaling)))
+            },
+        )
+
+        // 间距组
+        SectionTitle(title = stringResource(R.string.title_spacing))
+        TinySliderSettingItem(
+            title = stringResource(R.string.heading_spacing),
+            value = titleLineSpacingExtra.toFloat(),
+            valueRange = 0f..20f,
+            onValueChange = { value ->
+                titleLineSpacingExtra = value.toInt()
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleLineSpacingExtra(titleLineSpacingExtra)))
+            },
+        )
+        TinySliderSettingItem(
+            title = stringResource(R.string.subtitle_margin),
+            value = titleLineSpacingSub.toFloat(),
+            valueRange = -30f..30f,
+            onValueChange = { value ->
+                titleLineSpacingSub = value.toInt()
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleLineSpacingSub(titleLineSpacingSub)))
+            },
+        )
+
+        // 去重组
+        SectionTitle(title = stringResource(R.string.title_dedup))
+        TinySwitchSettingItem(
+            title = stringResource(R.string.same_title_removed),
+            checked = sameTitleRemoved,
+            imageVector = Icons.Default.CleanHands,
+            onCheckedChange = {
+                onIntent(ReadBookIntent.MenuSameTitleRemoved)
+            },
+        )
+
+        Spacer(Modifier.height(8.dp))
+    }
+
+    // Segmentation rule dialog
+    AppAlertDialog(
+        show = showFlagDialog,
+        onDismissRequest = { showFlagDialog = false },
+        title = stringResource(R.string.rule_segment),
+        content = {
+            AppTextField(
+                value = flagText,
+                onValueChange = { flagText = it },
+                singleLine = false,
+                modifier = Modifier.fillMaxWidth(),
+            )
+        },
+        confirmText = stringResource(android.R.string.ok),
+        onConfirm = {
+            titleSegFlag = flagText
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleSegFlag(titleSegFlag)))
+            showFlagDialog = false
+        },
+        dismissText = stringResource(android.R.string.cancel),
+        onDismiss = { showFlagDialog = false },
+    )
+}
+
+@Composable
+internal fun TypographyHeaderTab(
+    config: ReadSheetConfigUiState,
+    onIntent: (ReadBookIntent) -> Unit,
+    onOpenHeaderFontSelect: () -> Unit,
+    onOpenColorPicker: (TypographyColorTarget) -> Unit,
+    onOpenCustomTip: (CustomTipTarget) -> Unit,
+    headerMode: Int,
+    headerLeft: Int,
+    headerMiddle: Int,
+    headerRight: Int,
+    onHeaderModeChange: (Int) -> Unit,
+    onTipChange: (CustomTipTarget, Int) -> Unit,
+    showHeaderLine: Boolean,
+    onShowHeaderLineChange: (Boolean) -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    val context = LocalContext.current
+    val tipNames = remember { ReadBookConfig.tipNames }
+    val tipValues = remember { ReadBookConfig.tipValues }
+
+    Column(
+        modifier = modifier
+            .fillMaxWidth()
+            .verticalScroll(rememberScrollState()),
+    ) {
+        // 内容组
+        SectionTitle(title = stringResource(R.string.content))
+        val headerModes = ReadBookConfig.getHeaderModes(context)
+        TinyDropdownSettingItem(
+            title = stringResource(R.string.header),
+            selectedValue = headerMode.toString(),
+            displayEntries = headerModes.values.toTypedArray(),
+            entryValues = headerModes.keys.map { it.toString() }.toTypedArray(),
+            onValueChange = { onHeaderModeChange(it.toInt()) },
+        )
+        TipPositionDropdown(
+            label = stringResource(R.string.left),
+            value = headerLeft,
+            tipNames = tipNames,
+            tipValues = tipValues,
+            onValueChange = { onTipChange(CustomTipTarget.HEADER_LEFT, it) },
+        )
+        TipPositionDropdown(
+            label = stringResource(R.string.middle),
+            value = headerMiddle,
+            tipNames = tipNames,
+            tipValues = tipValues,
+            onValueChange = { onTipChange(CustomTipTarget.HEADER_MIDDLE, it) },
+        )
+        TipPositionDropdown(
+            label = stringResource(R.string.right),
+            value = headerRight,
+            tipNames = tipNames,
+            tipValues = tipValues,
+            onValueChange = { onTipChange(CustomTipTarget.HEADER_RIGHT, it) },
+        )
+
+        // 分隔线组
+        SectionTitle(title = stringResource(R.string.read_config_divider_line))
+        TinySwitchSettingItem(
+            title = stringResource(R.string.showLine),
+            checked = showHeaderLine,
+            onCheckedChange = { onShowHeaderLineChange(it) },
+        )
+        TinyColorSettingItem(
+            title = stringResource(R.string.tip_divider_color),
+            colorValue = when (config.tipDividerColor) {
+                -1 -> context.getCompatColor(R.color.divider)
+                0 -> ReadBookConfig.textColor
+                else -> config.tipDividerColor
+            },
+            onClick = { onOpenColorPicker(TypographyColorTarget.Divider) },
+        )
+
+        // 字体组
+        SectionTitle(title = stringResource(R.string.text_font))
+        TinyClickableSettingItem(
+            title = stringResource(R.string.select_font),
+            imageVector = Icons.Default.TextFields,
+            onClick = onOpenHeaderFontSelect,
+        )
+        TinySliderSettingItem(
+            title = stringResource(R.string.title_font_size),
+            value = config.headerFontSize.toFloat(),
+            valueRange = 0f..100f,
+            onValueChange = { value ->
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderFontSize(value.toInt())))
+                onIntent(ReadBookIntent.SaveReadStyleConfig)
+            },
+        )
+        TinyColorModeSettingItem(
+            title = stringResource(R.string.title_color),
+            dayColor = if (config.tipHeaderColor != 0) config.tipHeaderColor else config.textColorDay,
+            nightColor = if (config.tipHeaderColorNight != 0) config.tipHeaderColorNight else config.textColorNight,
+            onClickColor = { isNight ->
+                if (isNight) {
+                    onOpenColorPicker(TypographyColorTarget.HeaderNight)
+                } else {
+                    onOpenColorPicker(TypographyColorTarget.Header)
+                }
+            },
+        )
+
+        Spacer(Modifier.height(8.dp))
+    }
+}
+
+@Composable
+internal fun TypographyFooterTab(
+    config: ReadSheetConfigUiState,
+    onIntent: (ReadBookIntent) -> Unit,
+    onOpenFooterFontSelect: () -> Unit,
+    onOpenColorPicker: (TypographyColorTarget) -> Unit,
+    onOpenCustomTip: (CustomTipTarget) -> Unit,
+    footerMode: Int,
+    footerLeft: Int,
+    footerMiddle: Int,
+    footerRight: Int,
+    onFooterModeChange: (Int) -> Unit,
+    onTipChange: (CustomTipTarget, Int) -> Unit,
+    showFooterLine: Boolean,
+    onShowFooterLineChange: (Boolean) -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    val context = LocalContext.current
+    val tipNames = remember { ReadBookConfig.tipNames }
+    val tipValues = remember { ReadBookConfig.tipValues }
+
+    Column(
+        modifier = modifier
+            .fillMaxWidth()
+            .verticalScroll(rememberScrollState()),
+    ) {
+        // 内容组
+        SectionTitle(title = stringResource(R.string.content))
+        val footerModes = ReadBookConfig.getFooterModes(context)
+        TinyDropdownSettingItem(
+            title = stringResource(R.string.footer),
+            selectedValue = footerMode.toString(),
+            displayEntries = footerModes.values.toTypedArray(),
+            entryValues = footerModes.keys.map { it.toString() }.toTypedArray(),
+            onValueChange = { onFooterModeChange(it.toInt()) },
+        )
+        TipPositionDropdown(
+            label = stringResource(R.string.left),
+            value = footerLeft,
+            tipNames = tipNames,
+            tipValues = tipValues,
+            onValueChange = { onTipChange(CustomTipTarget.FOOTER_LEFT, it) },
+        )
+        TipPositionDropdown(
+            label = stringResource(R.string.middle),
+            value = footerMiddle,
+            tipNames = tipNames,
+            tipValues = tipValues,
+            onValueChange = { onTipChange(CustomTipTarget.FOOTER_MIDDLE, it) },
+        )
+        TipPositionDropdown(
+            label = stringResource(R.string.right),
+            value = footerRight,
+            tipNames = tipNames,
+            tipValues = tipValues,
+            onValueChange = { onTipChange(CustomTipTarget.FOOTER_RIGHT, it) },
+        )
+
+        // 分隔线组
+        SectionTitle(title = stringResource(R.string.read_config_divider_line))
+        TinySwitchSettingItem(
+            title = stringResource(R.string.showLine),
+            checked = showFooterLine,
+            onCheckedChange = { onShowFooterLineChange(it) },
+        )
+
+        // 字体组
+        SectionTitle(title = stringResource(R.string.text_font))
+        TinySwitchSettingItem(
+            title = stringResource(R.string.apply_header_style),
+            checked = config.applyHeaderStyle,
+            onCheckedChange = {
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ApplyHeaderStyle(it)))
+            },
+        )
+        if (!config.applyHeaderStyle) {
+            TinyClickableSettingItem(
+                title = stringResource(R.string.select_font),
+                imageVector = Icons.Default.TextFields,
+                onClick = onOpenFooterFontSelect,
+            )
+            TinySliderSettingItem(
+                title = stringResource(R.string.title_font_size),
+                value = config.footerFontSize.toFloat(),
+                valueRange = 0f..100f,
+                onValueChange = { value ->
+                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterFontSize(value.toInt())))
+                    onIntent(ReadBookIntent.SaveReadStyleConfig)
+                },
+            )
+            TinyColorModeSettingItem(
+                title = stringResource(R.string.title_color),
+                dayColor = if (config.tipFooterColor != 0) config.tipFooterColor else config.textColorDay,
+                nightColor = if (config.tipFooterColorNight != 0) config.tipFooterColorNight else config.textColorNight,
+                onClickColor = { isNight ->
+                    if (isNight) {
+                        onOpenColorPicker(TypographyColorTarget.FooterNight)
+                    } else {
+                        onOpenColorPicker(TypographyColorTarget.Footer)
+                    }
+                },
+            )
+        }
+
+        Spacer(Modifier.height(8.dp))
+    }
+}
+
+@Composable
+internal fun TypographyMarginTab(
+    config: ReadSheetConfigUiState,
+    onIntent: (ReadBookIntent) -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    // Body padding state
+    var paddingTop by remember(config.paddingTop) { mutableFloatStateOf(config.paddingTop.toFloat()) }
+    var paddingBottom by remember(config.paddingBottom) { mutableFloatStateOf(config.paddingBottom.toFloat()) }
+    var paddingLeft by remember(config.paddingLeft) { mutableFloatStateOf(config.paddingLeft.toFloat()) }
+    var paddingRight by remember(config.paddingRight) { mutableFloatStateOf(config.paddingRight.toFloat()) }
+
+    // Title padding state
+    var titleTopSpacing by remember(config.titleTopSpacing) { mutableIntStateOf(config.titleTopSpacing) }
+    var titleBottomSpacing by remember(config.titleBottomSpacing) { mutableIntStateOf(config.titleBottomSpacing) }
+
+    // Header padding state
+    var headerPaddingTop by remember(config.headerPaddingTop) { mutableFloatStateOf(config.headerPaddingTop.toFloat()) }
+    var headerPaddingBottom by remember(config.headerPaddingBottom) { mutableFloatStateOf(config.headerPaddingBottom.toFloat()) }
+    var headerPaddingLeft by remember(config.headerPaddingLeft) { mutableFloatStateOf(config.headerPaddingLeft.toFloat()) }
+    var headerPaddingRight by remember(config.headerPaddingRight) { mutableFloatStateOf(config.headerPaddingRight.toFloat()) }
+
+    // Footer padding state
+    var footerPaddingTop by remember(config.footerPaddingTop) { mutableFloatStateOf(config.footerPaddingTop.toFloat()) }
+    var footerPaddingBottom by remember(config.footerPaddingBottom) { mutableFloatStateOf(config.footerPaddingBottom.toFloat()) }
+    var footerPaddingLeft by remember(config.footerPaddingLeft) { mutableFloatStateOf(config.footerPaddingLeft.toFloat()) }
+    var footerPaddingRight by remember(config.footerPaddingRight) { mutableFloatStateOf(config.footerPaddingRight.toFloat()) }
+
+    Column(
+        modifier = modifier
+            .fillMaxWidth()
+            .verticalScroll(rememberScrollState()),
+    ) {
+        // 正文边距
+        SectionTitle(title = stringResource(R.string.body_padding))
+        PaddingSliders(
+            top = paddingTop, bottom = paddingBottom,
+            left = paddingLeft, right = paddingRight,
+            onTopChange = {
+                paddingTop = it
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingTop(it.toInt())))
+            },
+            onBottomChange = {
+                paddingBottom = it
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingBottom(it.toInt())))
+            },
+            onLeftChange = {
+                paddingLeft = it
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingLeft(it.toInt())))
+            },
+            onRightChange = {
+                paddingRight = it
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.PaddingRight(it.toInt())))
+            },
+        )
+
+        // 标题边距
+        SectionTitle(title = stringResource(R.string.title_padding))
+        TinySliderSettingItem(
+            title = stringResource(R.string.title_margin_top),
+            value = titleTopSpacing.toFloat(),
+            valueRange = 0f..200f,
+            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,
+            onValueChange = { value ->
+                titleBottomSpacing = value.toInt()
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleBottomSpacing(titleBottomSpacing)))
+            },
+        )
+
+        // 页眉边距
+        SectionTitle(title = stringResource(R.string.header_padding))
+        PaddingSliders(
+            top = headerPaddingTop, bottom = headerPaddingBottom,
+            left = headerPaddingLeft, right = headerPaddingRight,
+            onTopChange = {
+                headerPaddingTop = it
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingTop(it.toInt())))
+            },
+            onBottomChange = {
+                headerPaddingBottom = it
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingBottom(it.toInt())))
+            },
+            onLeftChange = {
+                headerPaddingLeft = it
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingLeft(it.toInt())))
+            },
+            onRightChange = {
+                headerPaddingRight = it
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderPaddingRight(it.toInt())))
+            },
+        )
+
+        // 页脚边距
+        SectionTitle(title = stringResource(R.string.footer_padding))
+        PaddingSliders(
+            top = footerPaddingTop, bottom = footerPaddingBottom,
+            left = footerPaddingLeft, right = footerPaddingRight,
+            onTopChange = {
+                footerPaddingTop = it
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingTop(it.toInt())))
+            },
+            onBottomChange = {
+                footerPaddingBottom = it
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingBottom(it.toInt())))
+            },
+            onLeftChange = {
+                footerPaddingLeft = it
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingLeft(it.toInt())))
+            },
+            onRightChange = {
+                footerPaddingRight = it
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterPaddingRight(it.toInt())))
+            },
+        )
+
+        Spacer(Modifier.height(8.dp))
+    }
+}
+
+// endregion
+
+// region Hoisted modal sheet composables
+
+@Composable
+internal fun TypographyColorPickerSheet(
+    target: TypographyColorTarget,
+    config: ReadSheetConfigUiState,
+    onDismiss: () -> Unit,
+    onIntent: (ReadBookIntent) -> Unit,
+) {
+    val context = LocalContext.current
+    val initialColor = remember(target, config) {
+        when (target) {
+            TypographyColorTarget.Text -> config.textColor
+            TypographyColorTarget.TextAccent -> config.textAccentColor
+            TypographyColorTarget.Title ->
+                if (config.titleColor != 0) config.titleColor else config.textColorDay
+            TypographyColorTarget.TitleNight ->
+                if (config.titleColorNight != 0) config.titleColorNight else config.textColorNight
+            TypographyColorTarget.Header ->
+                if (config.tipHeaderColor != 0) config.tipHeaderColor
+                else config.textColorDay
+            TypographyColorTarget.HeaderNight ->
+                if (config.tipHeaderColorNight != 0) config.tipHeaderColorNight
+                else config.textColorNight
+            TypographyColorTarget.Footer ->
+                if (config.tipFooterColor != 0) config.tipFooterColor
+                else config.textColorDay
+            TypographyColorTarget.FooterNight ->
+                if (config.tipFooterColorNight != 0) config.tipFooterColorNight
+                else config.textColorNight
+            TypographyColorTarget.Divider -> when (config.tipDividerColor) {
+                -1 -> context.getCompatColor(R.color.divider)
+                0 -> config.textColor
+                else -> config.tipDividerColor
+            }
+        }
+    }
+
+    ColorPickerSheet(
+        show = true,
+        initialColor = initialColor,
+        onDismissRequest = onDismiss,
+        onColorSelected = { color ->
+            when (target) {
+                TypographyColorTarget.Text ->
+                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TextColor(color)))
+                TypographyColorTarget.TextAccent ->
+                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TextAccentColor(color)))
+                TypographyColorTarget.Title ->
+                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleColor(color)))
+                TypographyColorTarget.TitleNight ->
+                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleColorNight(color)))
+                TypographyColorTarget.Header ->
+                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderColor(color)))
+                TypographyColorTarget.HeaderNight ->
+                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderColorNight(color)))
+                TypographyColorTarget.Footer ->
+                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterColor(color)))
+                TypographyColorTarget.FooterNight ->
+                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterColorNight(color)))
+                TypographyColorTarget.Divider ->
+                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipDividerColor(color)))
+            }
+            onDismiss()
+        },
+    )
+}
+
+@Composable
+internal fun TypographyHeaderFontSelectSheet(
+    config: ReadSheetConfigUiState,
+    onDismiss: () -> Unit,
+    onIntent: (ReadBookIntent) -> Unit,
+) {
+    TypographyFontSelectSheet(
+        selectedFontPath = ReadBookConfig.headerFont,
+        onSelectFont = { fileDoc ->
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderFont(fileDoc.uri.toString())))
+            onIntent(ReadBookIntent.SaveReadStyleConfig)
+            onDismiss()
+        },
+        onSelectSystemTypeface = {
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderFont("")))
+            onIntent(ReadBookIntent.SaveReadStyleConfig)
+            onDismiss()
+        },
+        onDismiss = onDismiss,
+    )
+}
+
+@Composable
+internal fun TypographyFooterFontSelectSheet(
+    config: ReadSheetConfigUiState,
+    onDismiss: () -> Unit,
+    onIntent: (ReadBookIntent) -> Unit,
+) {
+    TypographyFontSelectSheet(
+        selectedFontPath = ReadBookConfig.footerFont,
+        onSelectFont = { fileDoc ->
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterFont(fileDoc.uri.toString())))
+            onIntent(ReadBookIntent.SaveReadStyleConfig)
+            onDismiss()
+        },
+        onSelectSystemTypeface = {
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterFont("")))
+            onIntent(ReadBookIntent.SaveReadStyleConfig)
+            onDismiss()
+        },
+        onDismiss = onDismiss,
+    )
+}
+
+@Composable
+internal fun TypographyCustomTipDialog(
+    target: CustomTipTarget,
+    onDismiss: () -> Unit,
+    onIntent: (ReadBookIntent) -> Unit,
+) {
+    val initialTemplate = remember(target) { target.customTemplate }
+
+    CustomTipDialog(
+        show = true,
+        initialTemplate = initialTemplate,
+        onConfirm = { template ->
+            applyTipValue(target, ReadBookConfig.tipCustom, onIntent)
+            target.applyTemplate(template, onIntent)
+            onDismiss()
+        },
+        onDismissRequest = onDismiss,
+    )
+}
+
+// endregion
+
+// region Private helpers
+
+@Composable
+private fun TypographyFontSelectSheet(
+    selectedFontPath: String,
+    onSelectFont: (io.legado.app.utils.FileDoc) -> Unit,
+    onSelectSystemTypeface: (Int) -> Unit,
+    onDismiss: () -> Unit,
+) {
+    val readSettingsRepository: ReadSettingsRepository = koinInject()
+    val preferences by readSettingsRepository.preferences.collectAsStateWithLifecycle(
+        initialValue = null
+    )
+    val fontFolderState = remember(preferences) {
+        val pref = preferences
+        if (pref == null) {
+            FontFolderState.Loading
+        } else {
+            FontFolderState.Loaded(pref.fontFolder.takeIf { it.isNotEmpty() }?.toUri())
+        }
+    }
+    val systemTypefaces = stringArrayResource(R.array.system_typefaces)
+
+    FontSelectSheet(
+        show = true,
+        title = stringResource(R.string.select_font),
+        folderState = fontFolderState,
+        selectedFontPath = selectedFontPath,
+        onDismissRequest = onDismiss,
+        onSelectFont = onSelectFont,
+        onSelectSystemTypeface = onSelectSystemTypeface,
+        onOpenFolderPicker = { /* handled by FontSelectSheet internally */ },
+        systemTypefaces = systemTypefaces,
+    )
+}
+
+private fun applyTipValue(
+    target: CustomTipTarget,
+    value: Int,
+    onIntent: (ReadBookIntent) -> Unit,
+) {
+    val configUpdate = when (target) {
+        CustomTipTarget.HEADER_LEFT -> ConfigUpdate.TipHeaderLeft(value)
+        CustomTipTarget.HEADER_MIDDLE -> ConfigUpdate.TipHeaderMiddle(value)
+        CustomTipTarget.HEADER_RIGHT -> ConfigUpdate.TipHeaderRight(value)
+        CustomTipTarget.FOOTER_LEFT -> ConfigUpdate.TipFooterLeft(value)
+        CustomTipTarget.FOOTER_MIDDLE -> ConfigUpdate.TipFooterMiddle(value)
+        CustomTipTarget.FOOTER_RIGHT -> ConfigUpdate.TipFooterRight(value)
+    }
+    onIntent(ReadBookIntent.UpdateConfig(configUpdate))
+}
+
+// endregion

+ 7 - 0
app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt

@@ -444,6 +444,13 @@ fun TocScreen(
                                     onIntent(TocIntent.ReverseToc)
                                 }
                             )
+                            RoundDropdownMenuItem(
+                                text = stringResource(R.string.update_toc),
+                                onClick = {
+                                    dismiss()
+                                    onIntent(TocIntent.UpdateToc)
+                                }
+                            )
                             PillDivider()
                             RoundDropdownMenuItem(
                                 text = stringResource(R.string.replace_rule_title),

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

@@ -8,6 +8,7 @@ import androidx.lifecycle.SavedStateHandle
 import androidx.lifecycle.viewModelScope
 import io.legado.app.R
 import io.legado.app.base.BaseRuleViewModel
+import io.legado.app.constant.AppLog
 import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
@@ -18,12 +19,17 @@ import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.domain.usecase.CacheBookChaptersUseCase
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.ContentProcessor
+import io.legado.app.help.book.isEpub
 import io.legado.app.help.book.isLocal
+import io.legado.app.help.book.isMobi
 import io.legado.app.help.bookmark.BookmarkExporter
 import io.legado.app.model.CacheBook
 import io.legado.app.model.ReadBook
 import io.legado.app.model.cache.CacheBookDownloadState
+import io.legado.app.model.localBook.EpubFile
 import io.legado.app.model.localBook.LocalBook
+import io.legado.app.model.localBook.MobiFile
+import io.legado.app.model.webBook.WebBook
 import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
 import io.legado.app.ui.widget.components.list.ListUiState
 import io.legado.app.ui.widget.components.list.SelectableItem
@@ -132,6 +138,7 @@ sealed interface TocIntent {
     data object ToggleSplitLongChapter : TocIntent
     data object ExpandAllVolumes : TocIntent
     data object CollapseAllVolumes : TocIntent
+    data object UpdateToc : TocIntent
 }
 
 sealed interface TocEffect {
@@ -543,6 +550,7 @@ class TocViewModel(
             TocIntent.ToggleSplitLongChapter -> toggleSplitLongChapter()
             TocIntent.ExpandAllVolumes -> expandAllVolumes()
             TocIntent.CollapseAllVolumes -> collapseAllVolumes()
+            TocIntent.UpdateToc -> updateToc()
         }
     }
 
@@ -555,6 +563,48 @@ class TocViewModel(
         //bookState.value = newBook
     }
 
+    fun updateToc() = execute {
+        val book = bookState.value ?: return@execute
+        if (book.isLocal) {
+            if (book.isEpub) {
+                BookHelp.clearCache(book)
+                EpubFile.clear()
+            }
+            if (book.isMobi) {
+                MobiFile.clear()
+            }
+            kotlin.runCatching {
+                LocalBook.getChapterList(book).let {
+                    appDb.bookChapterDao.delByBook(book.bookUrl)
+                    appDb.bookChapterDao.insert(*it.toTypedArray())
+                    appDb.bookDao.update(book)
+                }
+            }.onFailure {
+                AppLog.put("LoadTocError:${it.localizedMessage}", it)
+                _effects.tryEmit(TocEffect.ShowMessage(it.localizedMessage ?: "Error"))
+            }
+        } else {
+            val source = appDb.bookSourceDao.getBookSource(book.origin)
+            source?.let {
+                val oldBook = book.copy()
+                WebBook.getChapterListAwait(it, book, true)
+                    .onSuccess { cList ->
+                        if (oldBook.bookUrl == book.bookUrl) {
+                            appDb.bookDao.update(book)
+                        } else {
+                            appDb.bookDao.replace(oldBook, book)
+                            BookHelp.updateCacheFolder(oldBook, book)
+                        }
+                        appDb.bookChapterDao.delByBook(oldBook.bookUrl)
+                        appDb.bookChapterDao.insert(*cList.toTypedArray())
+                    }.onFailure {
+                        AppLog.put("LoadTocError:${it.localizedMessage}", it)
+                        _effects.tryEmit(TocEffect.ShowMessage(it.localizedMessage ?: "Error"))
+                    }
+            }
+        }
+    }
+
     fun toggleUseReplace() {
         viewModelScope.launch {
             readSettingsRepository.setTocUiUseReplace(!tocPreferences.value.useReplace)

+ 6 - 1
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt

@@ -33,13 +33,16 @@ class ReplaceRuleActivity : BaseComposeActivity() {
 
     companion object {
         const val EXTRA_START_ROUTE = "start_route"
+        const val EXTRA_BOOK_URL = "book_url"
         fun startIntent(
             context: Context,
-            editRoute: ReplaceEditRoute? = null
+            editRoute: ReplaceEditRoute? = null,
+            bookUrl: String? = null
         ): Intent = Intent(context, ReplaceRuleActivity::class.java).apply {
             editRoute?.let {
                 putExtra(EXTRA_START_ROUTE, Json.encodeToString(it))
             }
+            bookUrl?.let { putExtra(EXTRA_BOOK_URL, it) }
         }
     }
 
@@ -55,6 +58,7 @@ class ReplaceRuleActivity : BaseComposeActivity() {
             action()
         }
             val startRouteJson = intent.getStringExtra(EXTRA_START_ROUTE)
+            val bookUrl = intent.getStringExtra(EXTRA_BOOK_URL)
             val backStack = rememberNavBackStack(
                 remember(startRouteJson) {
                     resolveStartRoute(startRouteJson)
@@ -149,6 +153,7 @@ class ReplaceRuleActivity : BaseComposeActivity() {
                 entryProvider = entryProvider {
                     entry<ReplaceRuleRoute> {
                         ReplaceRuleRouteScreen(
+                            bookUrl = bookUrl,
                             onBackClick = { leaveScreen { finish() } },
                             onNavigateToEdit = { route -> backStack.add(route) }
                         )

+ 33 - 2
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleContract.kt

@@ -2,11 +2,16 @@ package io.legado.app.ui.replace
 
 import android.net.Uri
 import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
 import io.legado.app.data.entities.ReplaceRule
+import io.legado.app.ui.book.read.ContentProcessConfigUiState
+import io.legado.app.ui.book.read.ContentProcessItemUi
 import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
 import io.legado.app.ui.widget.components.list.InteractionState
 import io.legado.app.ui.widget.components.list.ListUiState
 import io.legado.app.ui.widget.components.list.SelectableItem
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
 
 @Immutable
 data class ReplaceRuleItemUi(
@@ -41,13 +46,23 @@ data class ReplaceRuleItemUi(
     )
 }
 
+@Stable
 data class ReplaceRuleUiState(
     override val items: List<ReplaceRuleItemUi> = emptyList(),
     override val selectedIds: Set<Long> = emptySet(),
     override val searchKey: String = "",
     val sortMode: String = "desc",
     val selectedGroup: String? = null,
-    val interaction: InteractionState = InteractionState()
+    val interaction: InteractionState = InteractionState(),
+    // Book-specific state (only active when bookUrl is provided)
+    val bookUrl: String? = null,
+    val replaceEnabled: Boolean = false,
+    val effectiveRules: ImmutableList<ReplaceRule> = persistentListOf(),
+    val chineseConvertActive: Boolean = false,
+    val reSegmentActive: Boolean = false,
+    val contentProcessState: ContentProcessConfigUiState = ContentProcessConfigUiState(),
+    val showEffectiveReplaces: Boolean = false,
+    val showContentProcesses: Boolean = false,
 ) : ListUiState<ReplaceRuleItemUi> {
     override val isSearch: Boolean get() = interaction.isSearchMode
     override val isLoading: Boolean get() = interaction.isUploading
@@ -87,6 +102,22 @@ sealed interface ReplaceRuleIntent {
     data class AddGroup(val group: String) : ReplaceRuleIntent
     data class DeleteGroup(val group: String) : ReplaceRuleIntent
     data class UpGroup(val oldGroup: String, val newGroup: String?) : ReplaceRuleIntent
+    // Book-specific intents (only when bookUrl is provided)
+    data class InitBookData(val bookUrl: String) : ReplaceRuleIntent
+    data object ToggleReplaceEnable : ReplaceRuleIntent
+    data object ShowEffectiveReplaces : ReplaceRuleIntent
+    data object ShowContentProcesses : ReplaceRuleIntent
+    data object DismissEffectiveReplaces : ReplaceRuleIntent
+    data object DismissContentProcesses : ReplaceRuleIntent
+    data class DisableEffectiveRule(val rule: ReplaceRule) : ReplaceRuleIntent
+    data object DisableChineseConverter : ReplaceRuleIntent
+    data object DisableReSegment : ReplaceRuleIntent
+    data class ToggleContentProcess(val id: String, val enabled: Boolean) : ReplaceRuleIntent
+    data class RequestDeleteContentProcess(val item: ContentProcessItemUi) : ReplaceRuleIntent
+    data object ConfirmDeleteContentProcess : ReplaceRuleIntent
+    data object DismissDeleteContentProcess : ReplaceRuleIntent
 }
 
-sealed interface ReplaceRuleEffect
+sealed interface ReplaceRuleEffect {
+    data class OpenReplaceEditor(val id: Long, val pattern: String?) : ReplaceRuleEffect
+}

+ 72 - 0
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt

@@ -44,6 +44,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.base.BaseRuleEvent
 import io.legado.app.data.entities.ReplaceRule
+import io.legado.app.ui.book.read.ReadBookIntent
+import io.legado.app.ui.book.read.sheet.ContentProcessesSheet
+import io.legado.app.ui.book.read.sheet.EffectiveReplacesSheet
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.widget.components.ActionItem
@@ -74,6 +77,7 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
 @Composable
 fun ReplaceRuleRouteScreen(
     viewModel: ReplaceRuleViewModel = koinViewModel(),
+    bookUrl: String? = null,
     onBackClick: () -> Unit,
     onNavigateToEdit: (ReplaceEditRoute) -> Unit,
 ) {
@@ -81,6 +85,12 @@ fun ReplaceRuleRouteScreen(
     val importState by viewModel.importState.collectAsStateWithLifecycle()
     val groups by viewModel.allGroups.collectAsStateWithLifecycle()
 
+    LaunchedEffect(bookUrl) {
+        if (!bookUrl.isNullOrBlank()) {
+            viewModel.onIntent(ReplaceRuleIntent.InitBookData(bookUrl))
+        }
+    }
+
     ReplaceRuleScreen(
         state = uiState,
         importState = importState,
@@ -351,6 +361,23 @@ fun ReplaceRuleScreen(
         },
         snackbarHostState = snackbarHostState,
         dropDownMenuContent = { dismiss ->
+            // Book-specific items (only when bookUrl is provided)
+            if (state.bookUrl != null) {
+                RoundDropdownMenuItem(
+                    text = stringResource(R.string.replace_purify),
+                    isSelected = state.replaceEnabled,
+                    onClick = { onIntent(ReplaceRuleIntent.ToggleReplaceEnable) },
+                )
+                RoundDropdownMenuItem(
+                    text = stringResource(R.string.effective_replaces),
+                    onClick = { dismiss(); onIntent(ReplaceRuleIntent.ShowEffectiveReplaces) }
+                )
+                RoundDropdownMenuItem(
+                    text = stringResource(R.string.content_processes),
+                    onClick = { dismiss(); onIntent(ReplaceRuleIntent.ShowContentProcesses) }
+                )
+                PillDivider()
+            }
             RoundDropdownMenuItem(
                 text = stringResource(R.string.import_str),
                 onClick = { showImportSheet = true; dismiss() }
@@ -500,4 +527,49 @@ fun ReplaceRuleScreen(
             }
         }
     }
+
+    // Book-specific sheets
+    EffectiveReplacesSheet(
+        show = state.showEffectiveReplaces,
+        effectiveRules = state.effectiveRules,
+        chineseConvertActive = state.chineseConvertActive,
+        reSegmentActive = state.reSegmentActive,
+        onDismissRequest = { onIntent(ReplaceRuleIntent.DismissEffectiveReplaces) },
+        onOpenReplaceEditor = { id, pattern ->
+            onIntent(ReplaceRuleIntent.DismissEffectiveReplaces)
+            onNavigateToEdit(ReplaceEditRoute(id = id, pattern = pattern))
+        },
+        onReplaceRuleChanged = { /* rules updated via DB flow */ },
+        onNavigateToTextEffects = {
+            onIntent(ReplaceRuleIntent.DismissEffectiveReplaces)
+        },
+        onOpenContentProcesses = {
+            onIntent(ReplaceRuleIntent.DismissEffectiveReplaces)
+            onIntent(ReplaceRuleIntent.ShowContentProcesses)
+        },
+        onDisableRule = { onIntent(ReplaceRuleIntent.DisableEffectiveRule(it)) },
+        onDisableChineseConverter = { onIntent(ReplaceRuleIntent.DisableChineseConverter) },
+        onDisableReSegment = { onIntent(ReplaceRuleIntent.DisableReSegment) },
+    )
+
+    val contentProcessIntent: (ReadBookIntent) -> Unit = { readBookIntent ->
+        when (readBookIntent) {
+            is ReadBookIntent.ToggleContentProcess ->
+                onIntent(ReplaceRuleIntent.ToggleContentProcess(readBookIntent.id, readBookIntent.enabled))
+            is ReadBookIntent.RequestDeleteContentProcess ->
+                onIntent(ReplaceRuleIntent.RequestDeleteContentProcess(readBookIntent.item))
+            ReadBookIntent.ConfirmDeleteContentProcess ->
+                onIntent(ReplaceRuleIntent.ConfirmDeleteContentProcess)
+            ReadBookIntent.DismissDeleteContentProcess ->
+                onIntent(ReplaceRuleIntent.DismissDeleteContentProcess)
+            else -> {}
+        }
+    }
+
+    ContentProcessesSheet(
+        show = state.showContentProcesses,
+        state = state.contentProcessState,
+        onIntent = contentProcessIntent,
+        onDismissRequest = { onIntent(ReplaceRuleIntent.DismissContentProcesses) },
+    )
 }

+ 178 - 1
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt

@@ -7,24 +7,39 @@ import io.legado.app.base.BaseRuleViewModel
 import io.legado.app.constant.AppPattern
 import io.legado.app.constant.PreferKey
 import io.legado.app.data.appDb
+import io.legado.app.data.entities.BookContentProcess
 import io.legado.app.data.entities.ReplaceRule
+import io.legado.app.data.repository.ReadSettingsRepository
 import io.legado.app.data.repository.ReplaceRuleRepository
 import io.legado.app.data.repository.UploadRepository
+import io.legado.app.domain.gateway.BookContentProcessGateway
+import io.legado.app.domain.model.TextProcessAction
+import io.legado.app.domain.model.TextProcessAnchor
 import io.legado.app.help.ReplaceAnalyzer
+import io.legado.app.help.config.AppConfig
+import io.legado.app.model.ReadBook
+import io.legado.app.ui.book.read.ContentProcessConfigUiState
+import io.legado.app.ui.book.read.ContentProcessItemUi
 import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
 import io.legado.app.ui.widget.components.list.InteractionState
 import io.legado.app.utils.GSON
+import io.legado.app.utils.fromJsonObject
 import io.legado.app.utils.getPrefString
 import io.legado.app.utils.isJsonArray
 import io.legado.app.utils.isJsonObject
 import io.legado.app.utils.putPrefString
 import io.legado.app.utils.splitNotBlank
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.flatMapLatest
@@ -36,7 +51,9 @@ import kotlinx.coroutines.withContext
 
 class ReplaceRuleViewModel(
     application: Application,
-    uploadRepository: UploadRepository
+    uploadRepository: UploadRepository,
+    private val bookContentProcessGateway: BookContentProcessGateway,
+    private val readSettingsRepository: ReadSettingsRepository,
 ) : BaseRuleViewModel<ReplaceRuleItemUi, ReplaceRule, Long, ReplaceRuleUiState>(
     application,
     ReplaceRuleUiState(interaction = InteractionState(isLoading = true)),
@@ -48,6 +65,46 @@ class ReplaceRuleViewModel(
     private val _group = MutableStateFlow<String?>(null)
     val group = _group.asStateFlow()
 
+    private val _effects = MutableSharedFlow<ReplaceRuleEffect>(extraBufferCapacity = 16)
+    val effects = _effects.asSharedFlow()
+
+    private data class BookSpecificState(
+        val bookUrl: String? = null,
+        val replaceEnabled: Boolean = false,
+        val effectiveRules: ImmutableList<ReplaceRule> = persistentListOf(),
+        val chineseConvertActive: Boolean = false,
+        val reSegmentActive: Boolean = false,
+        val contentProcessState: ContentProcessConfigUiState = ContentProcessConfigUiState(),
+        val showEffectiveReplaces: Boolean = false,
+        val showContentProcesses: Boolean = false,
+    )
+
+    private val _bookState = MutableStateFlow(BookSpecificState())
+
+    // Must use `by lazy` — super.uiState depends on rawDataFlow (declared below),
+    // and eager initialization would access it before construction completes.
+    override val uiState: StateFlow<ReplaceRuleUiState> by lazy {
+        combine(
+            super.uiState,
+            _bookState
+        ) { baseState, bookState ->
+            baseState.copy(
+                bookUrl = bookState.bookUrl,
+                replaceEnabled = bookState.replaceEnabled,
+                effectiveRules = bookState.effectiveRules,
+                chineseConvertActive = bookState.chineseConvertActive,
+                reSegmentActive = bookState.reSegmentActive,
+                contentProcessState = bookState.contentProcessState,
+                showEffectiveReplaces = bookState.showEffectiveReplaces,
+                showContentProcesses = bookState.showContentProcesses,
+            )
+        }.stateIn(
+            scope = viewModelScope,
+            started = SharingStarted.WhileSubscribed(5000),
+            initialValue = initialState
+        )
+    }
+
     val allGroups: StateFlow<List<String>> = repository.flowGroups()
         .stateIn(
             scope = viewModelScope,
@@ -105,6 +162,46 @@ class ReplaceRuleViewModel(
             is ReplaceRuleIntent.AddGroup -> addGroup(intent.group)
             is ReplaceRuleIntent.DeleteGroup -> delGroup(intent.group)
             is ReplaceRuleIntent.UpGroup -> upGroup(intent.oldGroup, intent.newGroup)
+            // Book-specific
+            is ReplaceRuleIntent.InitBookData -> initBookData(intent.bookUrl)
+            ReplaceRuleIntent.ToggleReplaceEnable -> toggleReplaceEnable()
+            ReplaceRuleIntent.ShowEffectiveReplaces -> _bookState.update { it.copy(showEffectiveReplaces = true) }
+            ReplaceRuleIntent.ShowContentProcesses -> {
+                _bookState.update { it.copy(showContentProcesses = true) }
+                loadContentProcesses()
+            }
+            ReplaceRuleIntent.DismissEffectiveReplaces -> _bookState.update { it.copy(showEffectiveReplaces = false) }
+            ReplaceRuleIntent.DismissContentProcesses -> _bookState.update { it.copy(showContentProcesses = false) }
+            is ReplaceRuleIntent.DisableEffectiveRule -> viewModelScope.launch {
+                repository.insert(intent.rule.copy(isEnabled = false))
+            }
+            ReplaceRuleIntent.DisableChineseConverter -> {
+                viewModelScope.launch { readSettingsRepository.setChineseConverterType(0) }
+                _bookState.update { it.copy(chineseConvertActive = false) }
+            }
+            ReplaceRuleIntent.DisableReSegment -> {
+                ReadBook.book?.setReSegment(false)
+                ReadBook.loadContent(false)
+                _bookState.update { it.copy(reSegmentActive = false) }
+            }
+            is ReplaceRuleIntent.ToggleContentProcess -> viewModelScope.launch {
+                bookContentProcessGateway.setEnabled(intent.id, intent.enabled)
+            }
+            is ReplaceRuleIntent.RequestDeleteContentProcess -> _bookState.update {
+                it.copy(contentProcessState = it.contentProcessState.copy(deleteItem = intent.item))
+            }
+            ReplaceRuleIntent.ConfirmDeleteContentProcess -> {
+                val deleteItem = _bookState.value.contentProcessState.deleteItem
+                if (deleteItem != null) {
+                    viewModelScope.launch { bookContentProcessGateway.delete(deleteItem.id) }
+                    _bookState.update {
+                        it.copy(contentProcessState = it.contentProcessState.copy(deleteItem = null))
+                    }
+                }
+            }
+            ReplaceRuleIntent.DismissDeleteContentProcess -> _bookState.update {
+                it.copy(contentProcessState = it.contentProcessState.copy(deleteItem = null))
+            }
         }
     }
 
@@ -309,4 +406,84 @@ class ReplaceRuleViewModel(
 
     private fun upGroup(oldGroup: String, newGroup: String?) =
         viewModelScope.launch { repository.upGroup(oldGroup, newGroup) }
+
+    //region Book-specific methods
+
+    fun emitOpenReplaceEditor(id: Long, pattern: String?) {
+        _effects.tryEmit(ReplaceRuleEffect.OpenReplaceEditor(id, pattern))
+    }
+
+    private fun initBookData(bookUrl: String) {
+        val book = ReadBook.book
+        val textChapter = ReadBook.curTextChapter
+        if (book != null && book.bookUrl == bookUrl) {
+            val effectiveRules = textChapter?.effectiveReplaceRules.orEmpty().toImmutableList()
+            val replaceEnabled = book.getUseReplaceRule(AppConfig.replaceEnableDefault)
+            val chineseConvertActive = readSettingsRepository.currentSettings.chineseConverterType > 0
+            val reSegmentActive = book.getReSegment()
+            _bookState.update {
+                it.copy(
+                    bookUrl = bookUrl,
+                    replaceEnabled = replaceEnabled,
+                    effectiveRules = effectiveRules,
+                    chineseConvertActive = chineseConvertActive,
+                    reSegmentActive = reSegmentActive,
+                )
+            }
+        }
+    }
+
+    private fun toggleReplaceEnable() {
+        ReadBook.book?.let { book ->
+            val enabled = !book.getUseReplaceRule(AppConfig.replaceEnableDefault)
+            book.setUseReplaceRule(enabled)
+            ReadBook.saveRead()
+            _bookState.update { it.copy(replaceEnabled = enabled) }
+        }
+    }
+
+    private fun loadContentProcesses() {
+        val book = ReadBook.book ?: return
+        val chapterIndex = ReadBook.durChapterIndex
+        _bookState.update {
+            it.copy(contentProcessState = it.contentProcessState.copy(isLoading = true, errorMessage = null))
+        }
+        viewModelScope.launch(Dispatchers.IO) {
+            runCatching {
+                bookContentProcessGateway.getForChapter(book.bookUrl, chapterIndex)
+                    .mapNotNull { it.toContentProcessItemUi() }
+                    .toImmutableList()
+            }.onSuccess { items ->
+                _bookState.update {
+                    it.copy(contentProcessState = it.contentProcessState.copy(isLoading = false, items = items))
+                }
+            }.onFailure { error ->
+                _bookState.update {
+                    it.copy(contentProcessState = it.contentProcessState.copy(
+                        isLoading = false,
+                        errorMessage = error.localizedMessage
+                    ))
+                }
+            }
+        }
+    }
+
+    private fun BookContentProcess.toContentProcessItemUi(): ContentProcessItemUi? {
+        val anchor = GSON.fromJsonObject<TextProcessAnchor>(anchorJson).getOrNull()
+            ?: return null
+        val action = GSON.fromJsonObject<TextProcessAction>(actionJson).getOrNull()
+            ?: return null
+        return ContentProcessItemUi(
+            id = id,
+            kind = kind,
+            actionType = action.type,
+            enabled = enabled && status == BookContentProcess.STATUS_ACTIVE,
+            chapterIndex = chapterIndex ?: anchor.chapterIndex,
+            selectedText = anchor.selectedText,
+            replacementText = action.replacement ?: action.text.orEmpty(),
+            createdAt = createdAt,
+        )
+    }
+
+    //endregion
 }

+ 3 - 3
app/src/main/res/layout/dialog_read_info.xml

@@ -84,7 +84,7 @@
             app:max="30"
             app:min="8"
             app:orientation="horizontal"
-            app:title="@string/header_font_size" />
+            app:title="@string/title_font_size" />
 
     </LinearLayout>
 
@@ -99,14 +99,14 @@
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             style="@style/Widget.Material3Expressive.Button.OutlinedButton"
-            android:text="@string/header_color" />
+            android:text="@string/title_color" />
 
         <io.legado.app.ui.widget.AccentColorButton
             android:id="@+id/btn_footer_color"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             style="@style/Widget.Material3Expressive.Button.OutlinedButton"
-            android:text="@string/footer_color" />
+            android:text="@string/title_color" />
 
         <io.legado.app.ui.widget.AccentColorButton
             android:id="@+id/btn_divider_color"

+ 11 - 8
app/src/main/res/values-zh-rCN/strings.xml

@@ -356,6 +356,10 @@
     <string name="padding_left">左边距</string>
     <string name="padding_right">右边距</string>
     <string name="padding_format">上: %1$d  下: %2$d  左: %3$d  右: %4$d</string>
+    <string name="body_padding">正文边距</string>
+    <string name="title_padding">标题边距</string>
+    <string name="header_padding">页眉边距</string>
+    <string name="footer_padding">页脚边距</string>
     <string name="check_book_source">校验书源</string>
     <string name="check_select_source">校验所选</string>
     <string name="progress_show">%1$s      进度 %2$d/%3$d</string>
@@ -662,7 +666,6 @@
     <string name="bg_color">背景颜色</string>
     <string name="use_bg_image">使用背景图片</string>
     <string name="text_color">正文</string>
-    <string name="footer_font_size">页脚字号</string>
     <string name="select_image">选择图片</string>
     <string name="group_manage">分组管理</string>
     <string name="group_add">新建分组</string>
@@ -856,6 +859,7 @@
     <string name="auto_change_source">自动换源</string>
     <string name="text_full_justify">文字两端对齐</string>
     <string name="text_bottom_justify">文字底部对齐</string>
+    <string name="text_alignment">对齐</string>
     <string name="auto_page_speed">自动翻页速度</string>
     <string name="sort_by_url">地址排序</string>
     <string name="backup_summary">本地和 WebDav 一起备份</string>
@@ -903,7 +907,7 @@
     <string name="read_config_regex_desc">根据正则表达式更换字体与颜色</string>
     <string name="read_config_header_footer_desc">配置页眉页脚模块、显示规则和颜色</string>
     <string name="read_config_header_footer_padding_desc">调整页眉页脚上下左右边距</string>
-    <string name="read_config_divider_line">分割线</string>
+    <string name="read_config_divider_line">分隔线</string>
     <string name="group_name">分组名称</string>
     <string name="note_content">备注</string>
     <string name="replace_enable_default_t">默认启用替换净化</string>
@@ -1539,7 +1543,7 @@
     <string name="theme_config_reset_success">已恢复默认主题配置</string>
     <string name="input_value_range">输入数值(%1$d-%2$d)</string>
     <string name="change_icon_success">图标已修改</string>
-    <string name="replace_rule_title_setting">配置替换规则</string>
+    <string name="replace_rule_title_setting">替换规则</string>
     <string name="more_setting">更多</string>
     <string name="read_chapter_total">共 %d 章</string>
     <string name="read_chapter_index">已读 %d 章</string>
@@ -1792,6 +1796,9 @@
 	<string name="subtitle_scale">次行缩放</string>
 	<string name="subtitle_margin">次行距离</string>
 	<string name="heading_spacing">标题段距</string>
+	<string name="title_segment">分段</string>
+	<string name="title_spacing">间距</string>
+	<string name="title_dedup">去重</string>
     <string name="follow_read_background">跟随阅读背景</string>
     <string name="bookshelf_total_count">共%1$d本</string>
     <string name="bookshelf_selected_count">已选%1$d本</string>
@@ -1840,11 +1847,7 @@
     <string name="border_style">边框样式</string>
     <string name="solid_border">实线</string>
     <string name="dashed_border">虚线</string>
-    <string name="header_color">页眉</string>
-    <string name="footer_color">页脚</string>
-    <string name="header_font">设置字体</string>
-    <string name="footer_font">页脚字体</string>
-    <string name="header_font_size">设置字号</string>
+    <string name="apply_header_style">应用页眉字体样式</string>
     <string name="source_tab_start">启动</string>
     <string name="r_startHtml">启动页html文本</string>
     <string name="r_startStyle">启动页style文本</string>

+ 11 - 8
app/src/main/res/values/strings.xml

@@ -386,6 +386,10 @@
     <string name="padding_left">Left</string>
     <string name="padding_right">Right</string>
     <string name="padding_format">Top: %1$d  Bottom: %2$d  Left: %3$d  Right: %4$d</string>
+    <string name="body_padding">Body Padding</string>
+    <string name="title_padding">Title Padding</string>
+    <string name="header_padding">Header Padding</string>
+    <string name="footer_padding">Footer Padding</string>
     <string name="check_book_source">Check book sources</string>
     <string name="check_select_source">Check the selected source</string>
     <string name="progress_show">%1$s      Progress %2$d/%3$d</string>
@@ -692,12 +696,7 @@
     <string name="bg_color">Background color</string>
     <string name="use_bg_image">Use Background Image</string>
     <string name="text_color">Text Color</string>
-    <string name="header_color">Header</string>
-    <string name="footer_color">Footer</string>
-    <string name="header_font">Header Font</string>
-    <string name="footer_font">Footer Font</string>
-    <string name="header_font_size">Header Font Size</string>
-    <string name="footer_font_size">Footer Font Size</string>
+    <string name="apply_header_style">Apply Header Font Style</string>
     <string name="select_image">Select a picture</string>
     <string name="group_manage">Group Management</string>
     <string name="group_add">New Group</string>
@@ -890,6 +889,7 @@
     <string name="auto_change_source">Change Source Automatically</string>
     <string name="text_full_justify">Text Justified</string>
     <string name="text_bottom_justify">Text Align Bottom</string>
+    <string name="text_alignment">Alignment</string>
     <string name="auto_page_speed">Auto scroll speed</string>
     <string name="sort_by_url">Sort by URL</string>
     <string name="backup_summary">Backup the local and WebDAV simultaneously</string>
@@ -1061,7 +1061,7 @@
     <string name="content_src">Content source code</string>
     <string name="list_src">List source code</string>
     <string name="title_font_size">Font Size</string>
-    <string name="title_color">Title Color</string>
+    <string name="title_color">Color</string>
     <string name="reset_to_body_color">Reset to body text color</string>
     <string name="title_margin_top">Margin Top</string>
     <string name="title_margin_bottom">Margin Bottom</string>
@@ -1599,7 +1599,7 @@
     <string name="theme_config_no_font_files">No font files</string>
     <string name="input_value_range">Enter value (%1$d-%2$d)</string>
     <string name="change_icon_success">Icon Updated</string>
-    <string name="replace_rule_title_setting">Configure Replace Rules</string>
+    <string name="replace_rule_title_setting">Replace Rules</string>
     <string name="more_setting">More Settings</string>
     <string name="read_chapter_total">Total %d Chapters</string>
     <string name="read_chapter_index">Read %d Chapters</string>
@@ -1846,6 +1846,9 @@
     <string name="filter_show_not_in_shelf_only">Show Only Books Not in Shelf</string>
     <string name="follow_read_background">Follow background</string>
     <string name="heading_spacing">Heading spacing</string>
+    <string name="title_segment">Segment</string>
+    <string name="title_spacing">Spacing</string>
+    <string name="title_dedup">Dedup</string>
     <string name="layout_setting">Layout settings</string>
     <string name="rule_segment">Rule</string>
     <string name="split_title">Split title</string>