Procházet zdrojové kódy

feat: 整理阅读配置和更多菜单

HapeLee před 3 měsíci
rodič
revize
02459ebc4a
31 změnil soubory, kde provedl 2152 přidání a 953 odebrání
  1. 1 0
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  2. 3 0
      app/src/main/java/io/legado/app/data/repository/ReadSettingsRepository.kt
  3. 20 0
      app/src/main/java/io/legado/app/data/repository/ReplaceRuleRepository.kt
  4. 1 0
      app/src/main/java/io/legado/app/domain/model/settings/ReadSettings.kt
  5. 28 0
      app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt
  6. 161 56
      app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt
  7. 12 23
      app/src/main/java/io/legado/app/ui/book/read/ReadBookScreen.kt
  8. 76 9
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  9. 3 3
      app/src/main/java/io/legado/app/ui/book/read/sheet/ContentProcessesSheet.kt
  10. 13 231
      app/src/main/java/io/legado/app/ui/book/read/sheet/FloatingBarIconConfigSheet.kt
  11. 22 58
      app/src/main/java/io/legado/app/ui/book/read/sheet/GlobalThemePage.kt
  12. 13 0
      app/src/main/java/io/legado/app/ui/book/read/sheet/MoreConfigSheet.kt
  13. 6 4
      app/src/main/java/io/legado/app/ui/book/read/sheet/ReadStyleSheet.kt
  14. 42 76
      app/src/main/java/io/legado/app/ui/book/read/sheet/ReaderBookSheet.kt
  15. 471 0
      app/src/main/java/io/legado/app/ui/book/read/sheet/ReaderMoreActionsSheet.kt
  16. 13 3
      app/src/main/java/io/legado/app/ui/book/read/sheet/SystemMenuPage.kt
  17. 600 0
      app/src/main/java/io/legado/app/ui/book/read/sheet/TextProcessingSheet.kt
  18. 43 142
      app/src/main/java/io/legado/app/ui/book/read/sheet/TextSelectMenuConfigSheet.kt
  19. 91 26
      app/src/main/java/io/legado/app/ui/book/read/sheet/TypographyPage.kt
  20. 162 220
      app/src/main/java/io/legado/app/ui/book/read/sheet/TypographyTabs.kt
  21. 4 3
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleContract.kt
  22. 1 70
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt
  23. 4 2
      app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt
  24. 272 0
      app/src/main/java/io/legado/app/ui/widget/components/ReorderableConfigList.kt
  25. 2 12
      app/src/main/java/io/legado/app/ui/widget/components/button/ToggleChip.kt
  26. 17 3
      app/src/main/java/io/legado/app/ui/widget/components/modalBottomSheet/AppModalBottomSheet.kt
  27. 28 1
      app/src/main/java/io/legado/app/ui/widget/components/settingItem/TinySettingItems.kt
  28. 14 4
      app/src/main/res/values-zh-rCN/strings.xml
  29. 8 2
      app/src/main/res/values-zh-rHK/strings.xml
  30. 8 2
      app/src/main/res/values-zh-rTW/strings.xml
  31. 13 3
      app/src/main/res/values/strings.xml

+ 1 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -361,6 +361,7 @@ object PreferKey {
     const val showTitleBarIcons = "showTitleBarIcons"
     const val showTitleBarIcons = "showTitleBarIcons"
     const val showMenuIcon = "showMenuIcon"
     const val showMenuIcon = "showMenuIcon"
     const val titleBarCompact = "titleBarCompact"
     const val titleBarCompact = "titleBarCompact"
+    const val moreActionsConfig = "moreActionsConfig"
     const val disableReturnKey = "disableReturnKey"
     const val disableReturnKey = "disableReturnKey"
     const val selectText = "selectText"
     const val selectText = "selectText"
     //我在干什么
     //我在干什么

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

@@ -443,6 +443,7 @@ class ReadSettingsRepository(
             chineseConverterType = compatDsValue(Keys.ChineseConverterType, 0),
             chineseConverterType = compatDsValue(Keys.ChineseConverterType, 0),
             showMenuIcon = compatDsValue(Keys.ShowMenuIcon, false),
             showMenuIcon = compatDsValue(Keys.ShowMenuIcon, false),
             titleBarCompact = compatDsValue(Keys.TitleBarCompact, false),
             titleBarCompact = compatDsValue(Keys.TitleBarCompact, false),
+            moreActionsConfig = compatDsValue(Keys.MoreActionsConfig, ""),
         )
         )
     }
     }
 
 
@@ -554,6 +555,7 @@ class ReadSettingsRepository(
         val ChineseConverterType = intPreferencesKey(PreferKey.chineseConverterType)
         val ChineseConverterType = intPreferencesKey(PreferKey.chineseConverterType)
         val ShowMenuIcon = booleanPreferencesKey(PreferKey.showMenuIcon)
         val ShowMenuIcon = booleanPreferencesKey(PreferKey.showMenuIcon)
         val TitleBarCompact = booleanPreferencesKey(PreferKey.titleBarCompact)
         val TitleBarCompact = booleanPreferencesKey(PreferKey.titleBarCompact)
+        val MoreActionsConfig = stringPreferencesKey(PreferKey.moreActionsConfig)
     }
     }
 }
 }
 
 
@@ -672,4 +674,5 @@ internal fun ReadSettings.toGatewayPrefMap(): Map<String, Any?> = mapOf(
     PreferKey.chineseConverterType to chineseConverterType,
     PreferKey.chineseConverterType to chineseConverterType,
     PreferKey.showMenuIcon to showMenuIcon,
     PreferKey.showMenuIcon to showMenuIcon,
     PreferKey.titleBarCompact to titleBarCompact,
     PreferKey.titleBarCompact to titleBarCompact,
+    PreferKey.moreActionsConfig to moreActionsConfig,
 )
 )

+ 20 - 0
app/src/main/java/io/legado/app/data/repository/ReplaceRuleRepository.kt

@@ -205,6 +205,26 @@ class ReplaceRuleRepository(
             }
             }
         }
         }
 
 
+    /**
+     * 把 [draggedId] 规则移动到 [anchorId] 规则旁边([afterAnchor] 为 true 时在其后,否则在其前)。
+     * 列表顺序始终按 sortOrder 升序,移动后统一重写全部规则序号。
+     */
+    suspend fun moveReplaceRule(draggedId: Long, anchorId: Long, afterAnchor: Boolean) {
+        withContext(Dispatchers.IO) {
+            val rules = dao.all
+            val draggedIndex = rules.indexOfFirst { it.id == draggedId }
+            if (draggedIndex < 0) return@withContext
+            val dragged = rules[draggedIndex]
+            val remaining = rules.toMutableList().apply { removeAt(draggedIndex) }
+            val anchorIndex = remaining.indexOfFirst { it.id == anchorId }
+            if (anchorIndex < 0) return@withContext
+            val insertIndex = (anchorIndex + if (afterAnchor) 1 else 0).coerceIn(0, remaining.size)
+            remaining.add(insertIndex, dragged)
+            val updated = remaining.mapIndexed { index, rule -> rule.copy(order = index + 1) }
+            dao.update(*updated.toTypedArray())
+        }
+    }
+
     suspend fun moveOrder(currentRules: List<ReplaceRule>, isDesc: Boolean = false) {
     suspend fun moveOrder(currentRules: List<ReplaceRule>, isDesc: Boolean = false) {
         withContext(Dispatchers.IO) {
         withContext(Dispatchers.IO) {
             val size = currentRules.size
             val size = currentRules.size

+ 1 - 0
app/src/main/java/io/legado/app/domain/model/settings/ReadSettings.kt

@@ -107,4 +107,5 @@ data class ReadSettings(
     val chineseConverterType: Int = 0,
     val chineseConverterType: Int = 0,
     val showMenuIcon: Boolean = false,
     val showMenuIcon: Boolean = false,
     val titleBarCompact: Boolean = false,
     val titleBarCompact: Boolean = false,
+    val moreActionsConfig: String = "",
 )
 )

+ 28 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookContract.kt

@@ -58,6 +58,8 @@ sealed interface ReadBookMenuRoute {
     data object ReadAloud : ReadBookMenuRoute
     data object ReadAloud : ReadBookMenuRoute
     data object AutoRead : ReadBookMenuRoute
     data object AutoRead : ReadBookMenuRoute
     data object TypographyConfig : ReadBookMenuRoute
     data object TypographyConfig : ReadBookMenuRoute
+    data object InformationConfig : ReadBookMenuRoute
+    data object PaddingConfig : ReadBookMenuRoute
 }
 }
 
 
 @Stable
 @Stable
@@ -228,6 +230,7 @@ data class ReadBookUiState(
     val effectiveReplaceCount: Int = 0,
     val effectiveReplaceCount: Int = 0,
     val effectiveContentProcessCount: Int = 0,
     val effectiveContentProcessCount: Int = 0,
     val effectiveReplaceRules: ImmutableList<ReplaceRule> = persistentListOf(),
     val effectiveReplaceRules: ImmutableList<ReplaceRule> = persistentListOf(),
+    val allReplaceRules: ImmutableList<ReplaceRuleItemUi> = persistentListOf(),
     val chineseConverterActive: Boolean = false,
     val chineseConverterActive: Boolean = false,
     // Translation
     // Translation
     val translationMode: Boolean = false,
     val translationMode: Boolean = false,
@@ -320,6 +323,16 @@ data class ContentProcessItemUi(
     val createdAt: Long,
     val createdAt: Long,
 )
 )
 
 
+@Stable
+data class ReplaceRuleItemUi(
+    val id: Long,
+    val name: String,
+    val group: String?,
+    val pattern: String,
+    val replacement: String,
+    val enabled: Boolean,
+)
+
 @Stable
 @Stable
 data class ReadMenuConfig(
 data class ReadMenuConfig(
     val titleBarIconPosition: Int = 3,
     val titleBarIconPosition: Int = 3,
@@ -355,6 +368,7 @@ data class ReadMenuConfig(
     val readMenuCustomIcons: ImmutableMap<String, String> = persistentMapOf(),
     val readMenuCustomIcons: ImmutableMap<String, String> = persistentMapOf(),
     val titleBarButtons: ImmutableList<ReadBookButtonConfigItem> = persistentListOf(),
     val titleBarButtons: ImmutableList<ReadBookButtonConfigItem> = persistentListOf(),
     val bottomBarButtons: ImmutableList<ReadBookButtonConfigItem> = persistentListOf(),
     val bottomBarButtons: ImmutableList<ReadBookButtonConfigItem> = persistentListOf(),
+    val moreActionItems: ImmutableList<ReadBookButtonConfigItem> = persistentListOf(),
     val showBrightnessView: String = "0",
     val showBrightnessView: String = "0",
     val brightnessVwPos: String = "1",
     val brightnessVwPos: String = "1",
     val readBrightness: Int = 100,
     val readBrightness: Int = 100,
@@ -372,6 +386,7 @@ data class ReadBookButtonConfigItem(
 internal val ReadBookButtonIds = listOf(
 internal val ReadBookButtonIds = listOf(
     "ai_summary",
     "ai_summary",
     "ai_rewrite",
     "ai_rewrite",
+    "more_actions",
     "search",
     "search",
     "auto_page",
     "auto_page",
     "catalog",
     "catalog",
@@ -387,6 +402,13 @@ internal val ReadBookButtonIds = listOf(
     "translate",
     "translate",
 )
 )
 
 
+internal val MoreActionIds = listOf(
+    "change_source", "refresh", "download", "edit_content", "add_bookmark",
+    "text_processing", "reverse_content", "re_segment",
+    "del_ruby", "del_h", "toc_rule", "charset", "image_style", "page_anim",
+    "simulated_reading", "get_progress", "cover_progress", "log",
+)
+
 sealed interface ReadBookIntent {
 sealed interface ReadBookIntent {
     // Initialization
     // Initialization
     data class InitData(val intent: android.content.Intent) : ReadBookIntent
     data class InitData(val intent: android.content.Intent) : ReadBookIntent
@@ -437,6 +459,9 @@ sealed interface ReadBookIntent {
     data object RefreshAllChapters : ReadBookIntent
     data object RefreshAllChapters : ReadBookIntent
     data object RefreshContentAfter : ReadBookIntent
     data object RefreshContentAfter : ReadBookIntent
     data class ChangeReplaceRule(val enabled: Boolean) : ReadBookIntent
     data class ChangeReplaceRule(val enabled: Boolean) : ReadBookIntent
+    data class SetReplaceRuleEnabled(val id: Long, val enabled: Boolean) : ReadBookIntent
+    data class MoveReplaceRule(val draggedId: Long, val anchorId: Long, val afterAnchor: Boolean) :
+        ReadBookIntent
     data object ToggleTranslation : ReadBookIntent
     data object ToggleTranslation : ReadBookIntent
     data object OpenChapterSummary : ReadBookIntent
     data object OpenChapterSummary : ReadBookIntent
     data object OpenAiCurrentChapterRewrite : ReadBookIntent
     data object OpenAiCurrentChapterRewrite : ReadBookIntent
@@ -604,6 +629,7 @@ sealed interface ReadBookIntent {
     data class OpenTitleBarCustomIconPicker(val id: String) : ReadBookIntent
     data class OpenTitleBarCustomIconPicker(val id: String) : ReadBookIntent
     data class SaveMenuButtonConfig(val items: List<ReadBookButtonConfigItem>) : ReadBookIntent
     data class SaveMenuButtonConfig(val items: List<ReadBookButtonConfigItem>) : ReadBookIntent
     data class SaveTitleBarButtonConfig(val items: List<ReadBookButtonConfigItem>) : ReadBookIntent
     data class SaveTitleBarButtonConfig(val items: List<ReadBookButtonConfigItem>) : ReadBookIntent
+    data class SaveMoreActionsConfig(val items: List<ReadBookButtonConfigItem>) : ReadBookIntent
 
 
     // BgTextConfig (needs Activity for DialogFragment)
     // BgTextConfig (needs Activity for DialogFragment)
     data class OpenBgTextConfig(val index: Int) : ReadBookIntent
     data class OpenBgTextConfig(val index: Int) : ReadBookIntent
@@ -881,6 +907,7 @@ sealed interface ReadBookEffect {
 
 
 @Immutable
 @Immutable
 sealed interface ReadBookSheet {
 sealed interface ReadBookSheet {
+    data object MoreActions : ReadBookSheet
     data class BookNavigation(val initialTab: ReaderBookSheetTab) : ReadBookSheet
     data class BookNavigation(val initialTab: ReaderBookSheetTab) : ReadBookSheet
     data object PageAnim : ReadBookSheet
     data object PageAnim : ReadBookSheet
     data object Download : ReadBookSheet
     data object Download : ReadBookSheet
@@ -891,6 +918,7 @@ sealed interface ReadBookSheet {
     data object FloatingBarIconConfig : ReadBookSheet
     data object FloatingBarIconConfig : ReadBookSheet
     data object EffectiveReplaces : ReadBookSheet
     data object EffectiveReplaces : ReadBookSheet
     data object ContentProcesses : ReadBookSheet
     data object ContentProcesses : ReadBookSheet
+    data object TextProcessing : ReadBookSheet
     data object ContentEdit : ReadBookSheet
     data object ContentEdit : ReadBookSheet
     data object ChapterSummary : ReadBookSheet
     data object ChapterSummary : ReadBookSheet
     data object AiTextClean : ReadBookSheet
     data object AiTextClean : ReadBookSheet

+ 161 - 56
app/src/main/java/io/legado/app/ui/book/read/ReadBookMenuBar.kt

@@ -7,6 +7,7 @@ import androidx.compose.animation.SizeTransform
 import androidx.compose.animation.animateContentSize
 import androidx.compose.animation.animateContentSize
 import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.animation.core.animateDpAsState
 import androidx.compose.animation.core.animateFloatAsState
 import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.core.tween
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.fadeOut
 import androidx.compose.animation.slideInHorizontally
 import androidx.compose.animation.slideInHorizontally
@@ -66,13 +67,14 @@ import androidx.compose.material.icons.filled.Close
 import androidx.compose.material.icons.filled.CloudDownload
 import androidx.compose.material.icons.filled.CloudDownload
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.filled.Extension
 import androidx.compose.material.icons.filled.Extension
+import androidx.compose.material.icons.filled.FindReplace
+import androidx.compose.material.icons.filled.Image
 import androidx.compose.material.icons.filled.Menu
 import androidx.compose.material.icons.filled.Menu
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.Payment
 import androidx.compose.material.icons.filled.Payment
 import androidx.compose.material.icons.filled.Refresh
 import androidx.compose.material.icons.filled.Refresh
 import androidx.compose.material.icons.filled.Replay
 import androidx.compose.material.icons.filled.Replay
 import androidx.compose.material.icons.filled.Search
 import androidx.compose.material.icons.filled.Search
-import androidx.compose.material.icons.filled.Settings
 import androidx.compose.material.icons.filled.SwapHoriz
 import androidx.compose.material.icons.filled.SwapHoriz
 import androidx.compose.material.icons.filled.SwapVert
 import androidx.compose.material.icons.filled.SwapVert
 import androidx.compose.material.icons.filled.Sync
 import androidx.compose.material.icons.filled.Sync
@@ -84,6 +86,7 @@ import androidx.compose.material3.Surface
 import androidx.compose.material3.Text
 import androidx.compose.material3.Text
 import androidx.compose.material3.VerticalDivider
 import androidx.compose.material3.VerticalDivider
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableFloatStateOf
@@ -151,6 +154,7 @@ import dev.chrisbanes.haze.HazeState
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.constant.ReadMenuBlurMode
 import io.legado.app.constant.ReadMenuBlurMode
 import io.legado.app.constant.ReadMenuBlurStyle
 import io.legado.app.constant.ReadMenuBlurStyle
+import io.legado.app.data.entities.Book
 import io.legado.app.data.repository.ReadPreferences
 import io.legado.app.data.repository.ReadPreferences
 import io.legado.app.help.config.ReadStyleResolver
 import io.legado.app.help.config.ReadStyleResolver
 import io.legado.app.ui.animation.DampedDragAnimation
 import io.legado.app.ui.animation.DampedDragAnimation
@@ -159,6 +163,7 @@ 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.ReadMenuButtonInfo
 import io.legado.app.ui.book.read.sheet.ReadStyleContent
 import io.legado.app.ui.book.read.sheet.ReadStyleContent
 import io.legado.app.ui.book.read.sheet.TypographyPage
 import io.legado.app.ui.book.read.sheet.TypographyPage
+import io.legado.app.ui.book.read.sheet.TypographySection
 import io.legado.app.ui.book.read.sheet.readMenuButtonInfos
 import io.legado.app.ui.book.read.sheet.readMenuButtonInfos
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.AppSlider
 import io.legado.app.ui.widget.components.AppSlider
@@ -178,6 +183,8 @@ import io.legado.app.ui.widget.components.reader.readerMenuHazeEffect
 import io.legado.app.ui.widget.components.reader.readerMenuLiquidGlass
 import io.legado.app.ui.widget.components.reader.readerMenuLiquidGlass
 import io.legado.app.ui.widget.components.reader.readerMenuLiquidGlassAvailable
 import io.legado.app.ui.widget.components.reader.readerMenuLiquidGlassAvailable
 import io.legado.app.ui.widget.components.reader.readerMenuSurfaceBrush
 import io.legado.app.ui.widget.components.reader.readerMenuSurfaceBrush
+import io.legado.app.ui.widget.components.settingItem.LocalSliderDragState
+import io.legado.app.ui.widget.components.settingItem.SliderDragState
 import io.legado.app.ui.widget.components.text.AnimatedText
 import io.legado.app.ui.widget.components.text.AnimatedText
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.text.AppText
 import kotlin.math.ceil
 import kotlin.math.ceil
@@ -206,16 +213,21 @@ fun ReadBookMenuBar(
     } else {
     } else {
         ReadBookMenuContent.Route(currentRoute)
         ReadBookMenuContent.Route(currentRoute)
     }
     }
-    val dialogLikeRoute = currentRoute == ReadBookMenuRoute.TypographyConfig
+    val dialogLikeRoute = currentRoute == ReadBookMenuRoute.TypographyConfig ||
+            currentRoute == ReadBookMenuRoute.InformationConfig ||
+            currentRoute == ReadBookMenuRoute.PaddingConfig
     var readStylePage by remember { mutableIntStateOf(0) }
     var readStylePage by remember { mutableIntStateOf(0) }
+    val sliderDragState = remember { SliderDragState() }
     LaunchedEffect(currentRoute) {
     LaunchedEffect(currentRoute) {
         if (currentRoute != ReadBookMenuRoute.ReadStyle) {
         if (currentRoute != ReadBookMenuRoute.ReadStyle) {
             readStylePage = 0
             readStylePage = 0
         }
         }
+        sliderDragState.stopDragging()
     }
     }
     val hideTopBar = dialogLikeRoute
     val hideTopBar = dialogLikeRoute
     val menuColors = readMenuColors(preferences.readBarStyle)
     val menuColors = readMenuColors(preferences.readBarStyle)
 
 
+    CompositionLocalProvider(LocalSliderDragState provides sliderDragState) {
     Box(Modifier.fillMaxSize()) {
     Box(Modifier.fillMaxSize()) {
         AnimatedVisibility(
         AnimatedVisibility(
             visible = state.menuVisible || searchMenuVisible,
             visible = state.menuVisible || searchMenuVisible,
@@ -403,6 +415,7 @@ fun ReadBookMenuBar(
             }
             }
         }
         }
     }
     }
+    }
 }
 }
 
 
 private sealed interface ReadBookMenuContent {
 private sealed interface ReadBookMenuContent {
@@ -431,7 +444,9 @@ private fun ReadBookMenuSurface(
         is ReadBookMenuContent.Route -> contentTarget.route
         is ReadBookMenuContent.Route -> contentTarget.route
     }
     }
     val expanded = route != ReadBookMenuRoute.Main
     val expanded = route != ReadBookMenuRoute.Main
-    val dialogLikeRoute = route == ReadBookMenuRoute.TypographyConfig
+    val dialogLikeRoute = route == ReadBookMenuRoute.TypographyConfig ||
+            route == ReadBookMenuRoute.InformationConfig ||
+            route == ReadBookMenuRoute.PaddingConfig
     val density = LocalDensity.current
     val density = LocalDensity.current
     val windowSize = LocalWindowInfo.current.containerSize
     val windowSize = LocalWindowInfo.current.containerSize
     var surfaceHeightPx by remember { mutableIntStateOf(0) }
     var surfaceHeightPx by remember { mutableIntStateOf(0) }
@@ -530,12 +545,24 @@ private fun ReadBookMenuSurface(
     )
     )
     val bottomBarMenuTintColor = readMenuTintColor(state.menuConfig)
     val bottomBarMenuTintColor = readMenuTintColor(state.menuConfig)
         .takeIf { bottomBarVisualState.useTint }
         .takeIf { bottomBarVisualState.useTint }
-    val bottomBarSurfaceAlpha = state.menuConfig.readMenuBlurAlpha.coerceIn(0, 100) / 100f
-    val bottomBarFillAlpha = if (expanded) {
-        bottomBarSurfaceAlpha.coerceAtLeast(0.85f)
-    } else {
-        bottomBarSurfaceAlpha
-    }
+    val isSliderDragging = LocalSliderDragState.current?.isDragging == true
+    val menuSurfaceAlpha = state.menuConfig.readMenuBlurAlpha.coerceIn(0, 100)
+    // 拖动时降低不透明度(≤30%),露出背后排版以便实时预览;透明度变化带过渡动画
+    val dragMenuAlpha = menuSurfaceAlpha.coerceAtMost(30)
+    val animatedSurfaceAlpha by animateFloatAsState(
+        targetValue = if (isSliderDragging) dragMenuAlpha.toFloat() else menuSurfaceAlpha.toFloat(),
+        animationSpec = tween(durationMillis = 200),
+        label = "ReadMenuSurfaceAlpha",
+    )
+    val bottomBarFillAlpha by animateFloatAsState(
+        targetValue = when {
+            isSliderDragging -> dragMenuAlpha / 100f
+            expanded -> (menuSurfaceAlpha / 100f).coerceAtLeast(0.85f)
+            else -> menuSurfaceAlpha / 100f
+        },
+        animationSpec = tween(durationMillis = 200),
+        label = "ReadMenuFillAlpha",
+    )
     val bottomBarTextColor = Color(
     val bottomBarTextColor = Color(
         if (ReadStyleResolver.isNightTheme()) {
         if (ReadStyleResolver.isNightTheme()) {
             state.menuConfig.readMenuTextColorNight
             state.menuConfig.readMenuTextColorNight
@@ -581,6 +608,7 @@ private fun ReadBookMenuSurface(
                         useTopBarStyle = false,
                         useTopBarStyle = false,
                         useLens = useLens,
                         useLens = useLens,
                         menuConfig = state.menuConfig,
                         menuConfig = state.menuConfig,
+                        surfaceAlphaOverride = animatedSurfaceAlpha.roundToInt(),
                     )
                     )
                 } else {
                 } else {
                     Modifier
                     Modifier
@@ -595,6 +623,7 @@ private fun ReadBookMenuSurface(
                         menuConfig = state.menuConfig,
                         menuConfig = state.menuConfig,
                         visualState = bottomBarVisualState,
                         visualState = bottomBarVisualState,
                         blurRadiusDp = if (expanded) 32 else null,
                         blurRadiusDp = if (expanded) 32 else null,
+                        surfaceAlphaOverride = animatedSurfaceAlpha.roundToInt(),
                     )
                     )
                 } else {
                 } else {
                     Modifier
                     Modifier
@@ -695,8 +724,11 @@ private fun ReadBookMenuSurface(
                             onBack = { onIntent(ReadBookIntent.ReadMenuBack) },
                             onBack = { onIntent(ReadBookIntent.ReadMenuBack) },
                         ) {
                         ) {
                             ReadStyleContent(
                             ReadStyleContent(
-                                onOpenTypographyConfig = {
-                                    onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.TypographyConfig))
+                                onOpenInformationConfig = {
+                                    onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.InformationConfig))
+                                },
+                                onOpenPaddingConfig = {
+                                    onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.PaddingConfig))
                                 },
                                 },
                                 onOpenMoreConfig = {
                                 onOpenMoreConfig = {
                                     onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreConfig))
                                     onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreConfig))
@@ -704,8 +736,8 @@ private fun ReadBookMenuSurface(
                                 onOpenBgTextConfig = { index ->
                                 onOpenBgTextConfig = { index ->
                                     onIntent(ReadBookIntent.OpenBgTextConfig(index))
                                     onIntent(ReadBookIntent.OpenBgTextConfig(index))
                                 },
                                 },
-                                onOpenUnderlineConfig = {
-                                    onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.UnderlineConfig))
+                                onOpenTypographyConfig = {
+                                    onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.TypographyConfig))
                                 },
                                 },
                                 onOpenFontSelect = {
                                 onOpenFontSelect = {
                                     onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.FontSelect))
                                     onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.FontSelect))
@@ -752,6 +784,56 @@ private fun ReadBookMenuSurface(
                                     onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.HighlightRuleConfig))
                                     onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.HighlightRuleConfig))
                                 },
                                 },
                                 sameTitleRemoved = state.sameTitleRemoved,
                                 sameTitleRemoved = state.sameTitleRemoved,
+                                onOpenPaddingConfig = {
+                                    onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.PaddingConfig))
+                                },
+                            )
+                        }
+                    }
+
+                    ReadBookMenuRoute.InformationConfig -> {
+                        ReadBookMenuRoutePage(
+                            title = stringResource(R.string.information),
+                            maxHeight = maxHeight,
+                            scrollContent = false,
+                            animateSize = false,
+                            bottomPadding = if (extendSurfaceToNavigationBar) navBarHeight else 0.dp,
+                            onBack = { onIntent(ReadBookIntent.ReadMenuBack) },
+                        ) {
+                            TypographyPage(
+                                config = state.sheetConfig,
+                                onIntent = onIntent,
+                                onOpenFontSelect = {},
+                                onOpenTitleFontSelect = {},
+                                onOpenShadowSet = {},
+                                onOpenUnderlineConfig = {},
+                                onOpenHighlightRule = {},
+                                section = TypographySection.Information,
+                                onOpenPaddingConfig = {
+                                    onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.PaddingConfig))
+                                },
+                            )
+                        }
+                    }
+
+                    ReadBookMenuRoute.PaddingConfig -> {
+                        ReadBookMenuRoutePage(
+                            title = stringResource(R.string.padding),
+                            maxHeight = maxHeight,
+                            scrollContent = false,
+                            animateSize = false,
+                            bottomPadding = if (extendSurfaceToNavigationBar) navBarHeight else 0.dp,
+                            onBack = { onIntent(ReadBookIntent.ReadMenuBack) },
+                        ) {
+                            TypographyPage(
+                                config = state.sheetConfig,
+                                onIntent = onIntent,
+                                onOpenFontSelect = {},
+                                onOpenTitleFontSelect = {},
+                                onOpenShadowSet = {},
+                                onOpenUnderlineConfig = {},
+                                onOpenHighlightRule = {},
+                                section = TypographySection.Padding,
                             )
                             )
                         }
                         }
                     }
                     }
@@ -880,7 +962,6 @@ private fun MenuTitleBar(
     hazeState: HazeState?,
     hazeState: HazeState?,
     titleBarMode: String,
     titleBarMode: String,
 ) {
 ) {
-    var expanded by remember { mutableStateOf(false) }
 
 
     val topBarBorderWidth = state.menuConfig.readMenuBorderWidth
     val topBarBorderWidth = state.menuConfig.readMenuBorderWidth
     val topBarBorderColor = (if (ReadStyleResolver.isNightTheme()) {
     val topBarBorderColor = (if (ReadStyleResolver.isNightTheme()) {
@@ -889,7 +970,15 @@ private fun MenuTitleBar(
         state.menuConfig.readMenuBorderColor
         state.menuConfig.readMenuBorderColor
     }).takeIf { it != 0 }
     }).takeIf { it != 0 }
         ?: LegadoTheme.colorScheme.outlineVariant.hashCode()
         ?: LegadoTheme.colorScheme.outlineVariant.hashCode()
-    val topBarAlpha = state.menuConfig.readMenuBlurAlpha.coerceIn(0, 100) / 100f
+    val isSliderDragging = LocalSliderDragState.current?.isDragging == true
+    val topBarSurfaceAlpha = state.menuConfig.readMenuBlurAlpha.coerceIn(0, 100)
+    val dragTopBarAlpha = topBarSurfaceAlpha.coerceAtMost(30)
+    val animatedTopBarAlpha by animateFloatAsState(
+        targetValue = if (isSliderDragging) dragTopBarAlpha.toFloat() else topBarSurfaceAlpha.toFloat(),
+        animationSpec = tween(durationMillis = 200),
+        label = "ReadMenuTopBarAlpha",
+    )
+    val topBarAlpha = animatedTopBarAlpha / 100f
     val useTopBarBlur = readMenuTopBarHazeEnabled(hazeState, state.menuConfig)
     val useTopBarBlur = readMenuTopBarHazeEnabled(hazeState, state.menuConfig)
     val topBarVisualState = ReaderMenuVisualState(
     val topBarVisualState = ReaderMenuVisualState(
         effect = if (useTopBarBlur) ReaderMenuEffect.Haze else ReaderMenuEffect.None,
         effect = if (useTopBarBlur) ReaderMenuEffect.Haze else ReaderMenuEffect.None,
@@ -929,7 +1018,7 @@ private fun MenuTitleBar(
                         baseColor = colors.background,
                         baseColor = colors.background,
                         tintColor = readMenuTintColor(state.menuConfig),
                         tintColor = readMenuTintColor(state.menuConfig),
                         blurRadius = state.menuConfig.readMenuBlurRadius,
                         blurRadius = state.menuConfig.readMenuBlurRadius,
-                        surfaceAlpha = state.menuConfig.readMenuBlurAlpha,
+                        surfaceAlpha = animatedTopBarAlpha.roundToInt(),
                     )
                     )
                 } else {
                 } else {
                     Modifier.background(
                     Modifier.background(
@@ -1084,22 +1173,16 @@ private fun MenuTitleBar(
                         )
                         )
                     }
                     }
 
 
-                    Box {
-                        MenuTitleGlassButton(
-                            onClick = { expanded = true },
-                            icon = Icons.Default.MoreVert,
-                            contentDescription = stringResource(R.string.more_actions),
-                            state = state,
-                            colors = colors,
-                            backdrop = backdrop,
-                        )
-                        OverflowDropdownMenu(
-                            state = state,
-                            onIntent = onIntent,
-                            expanded = expanded,
-                            onDismiss = { expanded = false },
-                        )
-                    }
+                    MenuTitleGlassButton(
+                        onClick = {
+                            onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreActions))
+                        },
+                        icon = Icons.Default.MoreVert,
+                        contentDescription = stringResource(R.string.more_actions),
+                        state = state,
+                        colors = colors,
+                        backdrop = backdrop,
+                    )
                 }
                 }
             }
             }
         }
         }
@@ -1463,7 +1546,6 @@ private fun MenuTitleBarMergedGlassButton(
 ) {
 ) {
     var sourceExpanded by remember { mutableStateOf(false) }
     var sourceExpanded by remember { mutableStateOf(false) }
     var refreshExpanded by remember { mutableStateOf(false) }
     var refreshExpanded by remember { mutableStateOf(false) }
-    var overflowExpanded by remember { mutableStateOf(false) }
 
 
     val pillShape = RoundedCornerShape(50)
     val pillShape = RoundedCornerShape(50)
     val tint = LegadoTheme.colorScheme.onSurfaceVariant
     val tint = LegadoTheme.colorScheme.onSurfaceVariant
@@ -1701,7 +1783,7 @@ private fun MenuTitleBarMergedGlassButton(
                         indication = null,
                         indication = null,
                         interactionSource = remember { MutableInteractionSource() },
                         interactionSource = remember { MutableInteractionSource() },
                         role = Role.Button,
                         role = Role.Button,
-                        onClick = { overflowExpanded = true },
+                        onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreActions)) },
                     )
                     )
                     .semantics {
                     .semantics {
                         contentDescription = moreActionsDescription
                         contentDescription = moreActionsDescription
@@ -1748,12 +1830,6 @@ private fun MenuTitleBarMergedGlassButton(
             }
             }
         }
         }
 
 
-        OverflowDropdownMenu(
-            state = state,
-            onIntent = onIntent,
-            expanded = overflowExpanded,
-            onDismiss = { overflowExpanded = false },
-        )
     }
     }
 }
 }
 
 
@@ -2001,6 +2077,7 @@ private fun OverflowDropdownMenu(
         expanded = expanded,
         expanded = expanded,
         onDismissRequest = onDismiss,
         onDismissRequest = onDismiss,
     ) { dismiss ->
     ) { dismiss ->
+        var imageStyleExpanded by remember { mutableStateOf(false) }
 
 
         // Compact mode: moved from top bar buttons
         // Compact mode: moved from top bar buttons
         if (state.menuConfig.titleBarCompact) {
         if (state.menuConfig.titleBarCompact) {
@@ -2065,9 +2142,12 @@ private fun OverflowDropdownMenu(
             },
             },
         )
         )
         RoundDropdownMenuItem(
         RoundDropdownMenuItem(
-            text = stringResource(R.string.replace_rule_title_setting),
-            leadingIcon = menuIcon(Icons.Default.Settings),
-            onClick = { dismiss(); onIntent(ReadBookIntent.MenuSettingReplace) },
+            text = stringResource(R.string.text_processing),
+            leadingIcon = menuIcon(Icons.Default.FindReplace),
+            onClick = {
+                dismiss()
+                onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing))
+            },
         )
         )
         RoundDropdownMenuItem(
         RoundDropdownMenuItem(
             text = stringResource(R.string.reverse_content),
             text = stringResource(R.string.reverse_content),
@@ -2098,6 +2178,32 @@ private fun OverflowDropdownMenu(
         PillDivider()
         PillDivider()
 
 
         // 阅读设置
         // 阅读设置
+        Box {
+            RoundDropdownMenuItem(
+                text = stringResource(R.string.image_style),
+                leadingIcon = menuIcon(Icons.Default.Image),
+                onClick = { imageStyleExpanded = true },
+            )
+            RoundDropdownMenu(
+                expanded = imageStyleExpanded,
+                onDismissRequest = { imageStyleExpanded = false },
+            ) { subDismiss ->
+                listOf(
+                    R.string.btn_default_s to Book.imgStyleDefault,
+                    R.string.image_style_full to Book.imgStyleFull,
+                    R.string.image_style_text to Book.imgStyleText,
+                    R.string.image_style_single to Book.imgStyleSingle,
+                ).forEach { (label, style) ->
+                    RoundDropdownMenuItem(
+                        text = stringResource(label),
+                        onClick = {
+                            subDismiss()
+                            onIntent(ReadBookIntent.MenuImageStyle(style))
+                        },
+                    )
+                }
+            }
+        }
         RoundDropdownMenuItem(
         RoundDropdownMenuItem(
             text = stringResource(R.string.book_page_anim),
             text = stringResource(R.string.book_page_anim),
             leadingIcon = menuIcon(Icons.Default.Animation),
             leadingIcon = menuIcon(Icons.Default.Animation),
@@ -3023,13 +3129,11 @@ private fun loadToolButtons(
             onIntent(ReadBookIntent.NextChapter)
             onIntent(ReadBookIntent.NextChapter)
         },
         },
         infoMap.getValue("replace").toButton(
         infoMap.getValue("replace").toButton(
-            isActive = state.useReplaceRule,
-            onLongClick = { onIntent(ReadBookIntent.MenuSettingReplace) },
         ) {
         ) {
-            onIntent(ReadBookIntent.MenuEnableReplace)
+            onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing))
         },
         },
         infoMap.getValue("replace_badge").toButton {
         infoMap.getValue("replace_badge").toButton {
-            onIntent(ReadBookIntent.MenuSettingReplace)
+            onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing))
         },
         },
         infoMap.getValue("auto_page").toButton(isActive = state.isAutoPage) {
         infoMap.getValue("auto_page").toButton(isActive = state.isAutoPage) {
             if (state.isAutoPage) {
             if (state.isAutoPage) {
@@ -3048,6 +3152,9 @@ private fun loadToolButtons(
         infoMap.getValue("ai_rewrite").toButton {
         infoMap.getValue("ai_rewrite").toButton {
             onIntent(ReadBookIntent.OpenAiCurrentChapterRewrite)
             onIntent(ReadBookIntent.OpenAiCurrentChapterRewrite)
         },
         },
+        infoMap.getValue("more_actions").toButton {
+            onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreActions))
+        },
     )
     )
 
 
     val allMap = allButtons.associateBy { it.id }
     val allMap = allButtons.associateBy { it.id }
@@ -3144,10 +3251,11 @@ private fun Modifier.readMenuLiquidGlass(
     useLens: Boolean,
     useLens: Boolean,
     blurRadius: Dp? = null,
     blurRadius: Dp? = null,
     interactive: Boolean = false,
     interactive: Boolean = false,
+    surfaceAlphaOverride: Int? = null,
     menuConfig: ReadMenuConfig,
     menuConfig: ReadMenuConfig,
 ): Modifier {
 ): Modifier {
     val resolvedBlurRadius = blurRadius ?: menuConfig.readMenuBlurRadius.dp
     val resolvedBlurRadius = blurRadius ?: menuConfig.readMenuBlurRadius.dp
-    val blurAlpha = menuConfig.readMenuBlurAlpha
+    val blurAlpha = surfaceAlphaOverride ?: menuConfig.readMenuBlurAlpha
     val surfaceColor = readMenuTintColor(menuConfig) ?: colors.background
     val surfaceColor = readMenuTintColor(menuConfig) ?: colors.background
     val containerColor = surfaceColor.copy(
     val containerColor = surfaceColor.copy(
         alpha = (blurAlpha.coerceIn(0, 100) / 100f).coerceAtMost(0.6f)
         alpha = (blurAlpha.coerceIn(0, 100) / 100f).coerceAtMost(0.6f)
@@ -3188,6 +3296,7 @@ private fun Modifier.readMenuBottomBarHazeEffect(
     menuConfig: ReadMenuConfig,
     menuConfig: ReadMenuConfig,
     visualState: ReaderMenuVisualState,
     visualState: ReaderMenuVisualState,
     blurRadiusDp: Int? = null,
     blurRadiusDp: Int? = null,
+    surfaceAlphaOverride: Int? = null,
 ): Modifier {
 ): Modifier {
     return clip(shape)
     return clip(shape)
         .readerMenuHazeEffect(
         .readerMenuHazeEffect(
@@ -3197,7 +3306,7 @@ private fun Modifier.readMenuBottomBarHazeEffect(
             baseColor = colors.background,
             baseColor = colors.background,
             tintColor = readMenuTintColor(menuConfig),
             tintColor = readMenuTintColor(menuConfig),
             blurRadius = blurRadiusDp ?: menuConfig.readMenuBlurRadius,
             blurRadius = blurRadiusDp ?: menuConfig.readMenuBlurRadius,
-            surfaceAlpha = menuConfig.readMenuBlurAlpha,
+            surfaceAlpha = surfaceAlphaOverride ?: menuConfig.readMenuBlurAlpha,
         )
         )
 }
 }
 
 
@@ -3270,8 +3379,8 @@ private fun loadFloatingIcons(
         "eye_protection" to { onIntent(ReadBookIntent.ToggleEyeProtection) },
         "eye_protection" to { onIntent(ReadBookIntent.ToggleEyeProtection) },
         "prev_chapter" to { onIntent(ReadBookIntent.PrevChapter) },
         "prev_chapter" to { onIntent(ReadBookIntent.PrevChapter) },
         "next_chapter" to { onIntent(ReadBookIntent.NextChapter) },
         "next_chapter" to { onIntent(ReadBookIntent.NextChapter) },
-        "replace" to { onIntent(ReadBookIntent.MenuEnableReplace) },
-        "replace_badge" to { onIntent(ReadBookIntent.MenuSettingReplace) },
+        "replace" to { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing)) },
+        "replace_badge" to { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing)) },
         "auto_page" to {
         "auto_page" to {
             if (state.isAutoPage) {
             if (state.isAutoPage) {
                 onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.AutoRead))
                 onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.AutoRead))
@@ -3283,13 +3392,13 @@ private fun loadFloatingIcons(
         "translate" to { onIntent(ReadBookIntent.ToggleTranslation) },
         "translate" to { onIntent(ReadBookIntent.ToggleTranslation) },
         "ai_summary" to { onIntent(ReadBookIntent.OpenChapterSummary) },
         "ai_summary" to { onIntent(ReadBookIntent.OpenChapterSummary) },
         "ai_rewrite" to { onIntent(ReadBookIntent.OpenAiCurrentChapterRewrite) },
         "ai_rewrite" to { onIntent(ReadBookIntent.OpenAiCurrentChapterRewrite) },
+        "more_actions" to { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.MoreActions)) },
     )
     )
 
 
     val activeIds = buildSet {
     val activeIds = buildSet {
         if (state.isReadAloudRunning) add("read_aloud")
         if (state.isReadAloudRunning) add("read_aloud")
         if (state.isAutoPage) add("auto_page")
         if (state.isAutoPage) add("auto_page")
         if (state.translationMode) add("translate")
         if (state.translationMode) add("translate")
-        if (state.useReplaceRule) add("replace")
         if (eyeProtectionActive) add("eye_protection")
         if (eyeProtectionActive) add("eye_protection")
     }
     }
 
 
@@ -3310,10 +3419,6 @@ private fun loadFloatingIcons(
                         { onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.ReadAloud)) }
                         { onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.ReadAloud)) }
                     }
                     }
 
 
-                    "replace" -> {
-                        { onIntent(ReadBookIntent.MenuSettingReplace) }
-                    }
-
                     else -> null
                     else -> null
                 },
                 },
             )
             )

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

@@ -24,9 +24,7 @@ import io.legado.app.ui.book.read.sheet.ChapterSummarySheet
 import io.legado.app.ui.book.read.sheet.CharsetConfigSheet
 import io.legado.app.ui.book.read.sheet.CharsetConfigSheet
 import io.legado.app.ui.book.read.sheet.ClickActionConfigSheet
 import io.legado.app.ui.book.read.sheet.ClickActionConfigSheet
 import io.legado.app.ui.book.read.sheet.ContentEditSheet
 import io.legado.app.ui.book.read.sheet.ContentEditSheet
-import io.legado.app.ui.book.read.sheet.ContentProcessesSheet
 import io.legado.app.ui.book.read.sheet.DownloadSheet
 import io.legado.app.ui.book.read.sheet.DownloadSheet
-import io.legado.app.ui.book.read.sheet.EffectiveReplacesSheet
 import io.legado.app.ui.book.read.sheet.EyeProtectionConfigSheet
 import io.legado.app.ui.book.read.sheet.EyeProtectionConfigSheet
 import io.legado.app.ui.book.read.sheet.FloatingBarIconConfigSheet
 import io.legado.app.ui.book.read.sheet.FloatingBarIconConfigSheet
 import io.legado.app.ui.book.read.sheet.HighlightRuleConfigSheet
 import io.legado.app.ui.book.read.sheet.HighlightRuleConfigSheet
@@ -37,8 +35,10 @@ import io.legado.app.ui.book.read.sheet.PhotoSheet
 import io.legado.app.ui.book.read.sheet.ReadAloudNumberConfigSheet
 import io.legado.app.ui.book.read.sheet.ReadAloudNumberConfigSheet
 import io.legado.app.ui.book.read.sheet.ReadAloudPage
 import io.legado.app.ui.book.read.sheet.ReadAloudPage
 import io.legado.app.ui.book.read.sheet.ReadAloudScreen
 import io.legado.app.ui.book.read.sheet.ReadAloudScreen
+import io.legado.app.ui.book.read.sheet.ReaderMoreActionsSheet
 import io.legado.app.ui.book.read.sheet.ShadowSetSheet
 import io.legado.app.ui.book.read.sheet.ShadowSetSheet
 import io.legado.app.ui.book.read.sheet.SimulatedReadingSheet
 import io.legado.app.ui.book.read.sheet.SimulatedReadingSheet
+import io.legado.app.ui.book.read.sheet.TextProcessingSheet
 import io.legado.app.ui.book.read.sheet.ToolButtonConfigSheet
 import io.legado.app.ui.book.read.sheet.ToolButtonConfigSheet
 import io.legado.app.ui.book.read.sheet.UnderlineConfigSheet
 import io.legado.app.ui.book.read.sheet.UnderlineConfigSheet
 import io.legado.app.ui.book.readaloud.player.ReadAloudPlayerEffect
 import io.legado.app.ui.book.readaloud.player.ReadAloudPlayerEffect
@@ -176,30 +176,19 @@ fun ReadBookScreen(
         onDismissRequest = dismissSheet,
         onDismissRequest = dismissSheet,
         onIntent = onIntent,
         onIntent = onIntent,
     )
     )
-    EffectiveReplacesSheet(
-        show = state.activeSheet is ReadBookSheet.EffectiveReplaces,
+    TextProcessingSheet(
+        show = state.activeSheet is ReadBookSheet.TextProcessing,
+        book = state.book,
+        allRules = state.allReplaceRules,
         effectiveRules = state.effectiveReplaceRules,
         effectiveRules = state.effectiveReplaceRules,
-        chineseConvertActive = state.chineseConverterActive,
-        reSegmentActive = state.reSegment,
+        replaceEnabled = state.useReplaceRule,
+        contentProcessState = contentProcessState,
+        onIntent = onIntent,
         onDismissRequest = dismissSheet,
         onDismissRequest = dismissSheet,
-        onOpenReplaceEditor = { id, pattern ->
-            onIntent(ReadBookIntent.OpenReplaceEditor(id, pattern))
-        },
-        onReplaceRuleChanged = { onIntent(ReadBookIntent.ReplaceRuleChanged) },
-        onNavigateToTextEffects = {
-            onIntent(ReadBookIntent.DismissSheet)
-            onIntent(ReadBookIntent.OpenReadMenuRoute(ReadBookMenuRoute.TypographyConfig))
-        },
-        onOpenContentProcesses = {
-            onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ContentProcesses))
-        },
-        onDisableRule = { onIntent(ReadBookIntent.DisableEffectiveReplace(it)) },
-        onDisableChineseConverter = { onIntent(ReadBookIntent.DisableChineseConverter) },
-        onDisableReSegment = { onIntent(ReadBookIntent.DisableReSegment) },
     )
     )
-    ContentProcessesSheet(
-        show = state.activeSheet is ReadBookSheet.ContentProcesses,
-        state = contentProcessState,
+    ReaderMoreActionsSheet(
+        show = state.activeSheet is ReadBookSheet.MoreActions,
+        state = state,
         onIntent = onIntent,
         onIntent = onIntent,
         onDismissRequest = dismissSheet,
         onDismissRequest = dismissSheet,
     )
     )

+ 76 - 9
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -15,7 +15,6 @@ import io.legado.app.constant.PreferKey
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookProgress
 import io.legado.app.data.entities.BookProgress
-import io.legado.app.data.entities.Bookmark
 import io.legado.app.data.local.preferences.LocalPreferencesKeys
 import io.legado.app.data.local.preferences.LocalPreferencesKeys
 import io.legado.app.data.repository.BookRepository
 import io.legado.app.data.repository.BookRepository
 import io.legado.app.data.repository.BookSourceRepository
 import io.legado.app.data.repository.BookSourceRepository
@@ -59,7 +58,6 @@ import io.legado.app.help.book.isMobi
 import io.legado.app.help.book.removeType
 import io.legado.app.help.book.removeType
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.coroutine.Coroutine
-import io.legado.app.help.http.text
 import io.legado.app.help.source.getSourceType
 import io.legado.app.help.source.getSourceType
 import io.legado.app.model.ImageProvider
 import io.legado.app.model.ImageProvider
 import io.legado.app.model.ReadAloud
 import io.legado.app.model.ReadAloud
@@ -76,8 +74,8 @@ import io.legado.app.model.translation.TranslationChapterKey
 import io.legado.app.model.translation.TranslationChapterStatus
 import io.legado.app.model.translation.TranslationChapterStatus
 import io.legado.app.model.translation.TranslationManager
 import io.legado.app.model.translation.TranslationManager
 import io.legado.app.service.BaseReadAloudService
 import io.legado.app.service.BaseReadAloudService
-import io.legado.app.ui.book.read.sheet.ReaderBookSheetTab
 import io.legado.app.ui.book.read.page.entities.TextChapter
 import io.legado.app.ui.book.read.page.entities.TextChapter
+import io.legado.app.ui.book.read.sheet.ReaderBookSheetTab
 import io.legado.app.ui.book.searchContent.SearchResult
 import io.legado.app.ui.book.searchContent.SearchResult
 import io.legado.app.utils.ImageSaveUtils
 import io.legado.app.utils.ImageSaveUtils
 import io.legado.app.utils.NetworkUtils
 import io.legado.app.utils.NetworkUtils
@@ -88,7 +86,6 @@ import io.legado.app.utils.toStringArray
 import kotlinx.collections.immutable.toImmutableList
 import kotlinx.collections.immutable.toImmutableList
 import kotlinx.collections.immutable.toImmutableMap
 import kotlinx.collections.immutable.toImmutableMap
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Dispatchers.IO
-import kotlinx.coroutines.Dispatchers.Main
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.delay
@@ -98,13 +95,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.callbackFlow
 import kotlinx.coroutines.flow.callbackFlow
-import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.first
 import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.takeWhile
 import kotlinx.coroutines.flow.takeWhile
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
-import kotlin.coroutines.coroutineContext
 
 
 /**
 /**
  * 阅读界面 ViewModel — MVI/UDF 架构
  * 阅读界面 ViewModel — MVI/UDF 架构
@@ -544,9 +538,31 @@ class ReadBookViewModel(
         collectEventBus()
         collectEventBus()
         collectReaderSession()
         collectReaderSession()
         collectReadStyle()
         collectReadStyle()
+        collectReplaceRules()
         execute { readAloudDelegate.syncConfiguredTtsVoices() }
         execute { readAloudDelegate.syncConfiguredTtsVoices() }
     }
     }
 
 
+    private fun collectReplaceRules() {
+        viewModelScope.launch {
+            replaceRuleRepository.flowAll().collect { rules ->
+                _uiState.update { state ->
+                    state.copy(
+                        allReplaceRules = rules.map { rule ->
+                            ReplaceRuleItemUi(
+                                id = rule.id,
+                                name = rule.name,
+                                group = rule.group,
+                                pattern = rule.pattern,
+                                replacement = rule.replacement,
+                                enabled = rule.isEnabled,
+                            )
+                        }.toImmutableList()
+                    )
+                }
+            }
+        }
+    }
+
     /**
     /**
      * 消费 [ReadStyleGateway.state](Track E · E2):排版配置的唯一变更通知。
      * 消费 [ReadStyleGateway.state](Track E · E2):排版配置的唯一变更通知。
      *
      *
@@ -770,6 +786,19 @@ class ReadBookViewModel(
             is ReadBookIntent.RefreshAllChapters -> refreshAllChapters()
             is ReadBookIntent.RefreshAllChapters -> refreshAllChapters()
             is ReadBookIntent.RefreshContentAfter -> refreshContentAfter()
             is ReadBookIntent.RefreshContentAfter -> refreshContentAfter()
             is ReadBookIntent.ChangeReplaceRule -> changeReplaceRule(intent.enabled)
             is ReadBookIntent.ChangeReplaceRule -> changeReplaceRule(intent.enabled)
+            is ReadBookIntent.SetReplaceRuleEnabled -> viewModelScope.launch {
+                replaceRuleRepository.setEnabled(intent.id, intent.enabled)
+                replaceRuleChanged()
+            }
+
+            is ReadBookIntent.MoveReplaceRule -> viewModelScope.launch {
+                replaceRuleRepository.moveReplaceRule(
+                    intent.draggedId,
+                    intent.anchorId,
+                    intent.afterAnchor
+                )
+                replaceRuleChanged()
+            }
             is ReadBookIntent.DisableEffectiveReplace -> viewModelScope.launch {
             is ReadBookIntent.DisableEffectiveReplace -> viewModelScope.launch {
                 replaceRuleRepository.insert(intent.rule.copy(isEnabled = false))
                 replaceRuleRepository.insert(intent.rule.copy(isEnabled = false))
             }
             }
@@ -870,7 +899,10 @@ class ReadBookViewModel(
                 if (intent.sheet is ReadBookSheet.HighlightRuleConfig) {
                 if (intent.sheet is ReadBookSheet.HighlightRuleConfig) {
                     highlightRuleDelegate.load()
                     highlightRuleDelegate.load()
                     _uiState.update { it.copy(activeSheet = intent.sheet) }
                     _uiState.update { it.copy(activeSheet = intent.sheet) }
-                } else if (intent.sheet is ReadBookSheet.ContentProcesses) {
+                } else if (
+                    intent.sheet is ReadBookSheet.ContentProcesses ||
+                    intent.sheet is ReadBookSheet.TextProcessing
+                ) {
                     _uiState.update { it.copy(activeSheet = intent.sheet) }
                     _uiState.update { it.copy(activeSheet = intent.sheet) }
                     contentProcessDelegate.load()
                     contentProcessDelegate.load()
                 } else if (intent.sheet is ReadBookSheet.AiRewritePresetConfig) {
                 } else if (intent.sheet is ReadBookSheet.AiRewritePresetConfig) {
@@ -884,7 +916,8 @@ class ReadBookViewModel(
                 when (_uiState.value.activeSheet) {
                 when (_uiState.value.activeSheet) {
                     is ReadBookSheet.HighlightRuleConfig -> highlightRuleDelegate.onSheetDismissed()
                     is ReadBookSheet.HighlightRuleConfig -> highlightRuleDelegate.onSheetDismissed()
                     is ReadBookSheet.ContentEdit -> contentEditDelegate.onSheetDismissed()
                     is ReadBookSheet.ContentEdit -> contentEditDelegate.onSheetDismissed()
-                    is ReadBookSheet.ContentProcesses -> contentProcessDelegate.onSheetDismissed()
+                    is ReadBookSheet.ContentProcesses,
+                    is ReadBookSheet.TextProcessing -> contentProcessDelegate.onSheetDismissed()
                     else -> Unit
                     else -> Unit
                 }
                 }
                 _uiState.update { it.copy(activeSheet = null) }
                 _uiState.update { it.copy(activeSheet = null) }
@@ -1140,6 +1173,22 @@ class ReadBookViewModel(
                 buttonConfigDelegate.saveMenuButtons(intent.items)
                 buttonConfigDelegate.saveMenuButtons(intent.items)
             is ReadBookIntent.SaveTitleBarButtonConfig ->
             is ReadBookIntent.SaveTitleBarButtonConfig ->
                 buttonConfigDelegate.saveTitleBarButtons(intent.items)
                 buttonConfigDelegate.saveTitleBarButtons(intent.items)
+            is ReadBookIntent.SaveMoreActionsConfig -> viewModelScope.launch {
+                val seen = mutableSetOf<String>()
+                val normalized = intent.items.mapNotNull { item ->
+                    item.takeIf { it.id in MoreActionIds && seen.add(it.id) }
+                }.toMutableList()
+                MoreActionIds.forEach { id ->
+                    if (seen.add(id)) normalized.add(ReadBookButtonConfigItem(id, true))
+                }
+                readSettingsRepository.update { settings ->
+                    settings.copy(
+                        moreActionsConfig = normalized.joinToString(";") {
+                            "${it.id},${it.enabled}"
+                        }
+                    )
+                }
+            }
 
 
             is ReadBookIntent.KeepLightChanged -> {
             is ReadBookIntent.KeepLightChanged -> {
                 _readPreferences.update { it.copy(keepLight = intent.value) }
                 _readPreferences.update { it.copy(keepLight = intent.value) }
@@ -1886,6 +1935,9 @@ class ReadBookViewModel(
                 readMenuCustomIcons = ReadBookConfig.readMenuCustomIcons.toImmutableMap(),
                 readMenuCustomIcons = ReadBookConfig.readMenuCustomIcons.toImmutableMap(),
                 titleBarButtons = current.menuConfig.titleBarButtons,
                 titleBarButtons = current.menuConfig.titleBarButtons,
                 bottomBarButtons = current.menuConfig.bottomBarButtons,
                 bottomBarButtons = current.menuConfig.bottomBarButtons,
+                moreActionItems = readSettingsRepository.currentSettings.moreActionsConfig
+                    .toMoreActionConfig()
+                    .toImmutableList(),
                 showBrightnessView = ReadBookConfig.showBrightnessView,
                 showBrightnessView = ReadBookConfig.showBrightnessView,
                 brightnessVwPos = ReadBookConfig.brightnessVwPos,
                 brightnessVwPos = ReadBookConfig.brightnessVwPos,
                 readBrightness = ReadBookConfig.readBrightness,
                 readBrightness = ReadBookConfig.readBrightness,
@@ -1896,6 +1948,21 @@ class ReadBookViewModel(
         )
         )
     }
     }
 
 
+    private fun String.toMoreActionConfig(): List<ReadBookButtonConfigItem> {
+        if (isBlank()) return MoreActionIds.map { ReadBookButtonConfigItem(it, true) }
+        val seen = mutableSetOf<String>()
+        val items = split(";").mapNotNull { token ->
+            val parts = token.split(",")
+            val id = parts.getOrNull(0)?.takeIf { it in MoreActionIds && seen.add(it) }
+            val enabled = parts.getOrNull(1)?.toBooleanStrictOrNull()
+            if (id != null && enabled != null) ReadBookButtonConfigItem(id, enabled) else null
+        }.toMutableList()
+        MoreActionIds.forEach { id ->
+            if (seen.add(id)) items.add(ReadBookButtonConfigItem(id, true))
+        }
+        return items
+    }
+
     private fun observeCurrentTranslation(
     private fun observeCurrentTranslation(
         book: Book?,
         book: Book?,
         chapterIndex: Int,
         chapterIndex: Int,

+ 3 - 3
app/src/main/java/io/legado/app/ui/book/read/sheet/ContentProcessesSheet.kt

@@ -31,6 +31,7 @@ import io.legado.app.ui.book.read.ContentProcessConfigUiState
 import io.legado.app.ui.book.read.ContentProcessItemUi
 import io.legado.app.ui.book.read.ContentProcessItemUi
 import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.alert.AppAlertDialog
 import io.legado.app.ui.widget.components.button.series.MediumToggleButton
 import io.legado.app.ui.widget.components.button.series.MediumToggleButton
 import io.legado.app.ui.widget.components.button.series.MediumTonalButton
 import io.legado.app.ui.widget.components.button.series.MediumTonalButton
@@ -75,9 +76,8 @@ fun ContentProcessesSheet(
             }
             }
 
 
             state.items.isEmpty() -> {
             state.items.isEmpty() -> {
-                AppText(
-                    text = stringResource(R.string.content_process_empty),
-                    color = LegadoTheme.colorScheme.onSurfaceVariant,
+                EmptyMessage(
+                    message = stringResource(R.string.content_process_empty),
                     modifier = Modifier
                     modifier = Modifier
                         .fillMaxWidth(),
                         .fillMaxWidth(),
                 )
                 )

+ 13 - 231
app/src/main/java/io/legado/app/ui/book/read/sheet/FloatingBarIconConfigSheet.kt

@@ -1,53 +1,22 @@
 package io.legado.app.ui.book.read.sheet
 package io.legado.app.ui.book.read.sheet
 
 
 import android.content.Context
 import android.content.Context
-import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material.icons.filled.Menu
-import androidx.compose.material.icons.filled.Visibility
-import androidx.compose.material.icons.filled.VisibilityOff
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
-import coil.compose.AsyncImage
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.ui.book.read.ConfigUpdate
 import io.legado.app.ui.book.read.ConfigUpdate
 import io.legado.app.ui.book.read.ReadBookButtonConfigItem
 import io.legado.app.ui.book.read.ReadBookButtonConfigItem
 import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.book.read.ReadBookIntent
-import io.legado.app.ui.theme.LegadoTheme
-import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
-import io.legado.app.ui.widget.components.button.series.SmallTonalButton
-import io.legado.app.ui.widget.components.card.NormalCard
+import io.legado.app.ui.widget.components.ConfigListEntry
+import io.legado.app.ui.widget.components.ReorderableConfigList
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
-import io.legado.app.ui.widget.components.reorderAccessibility
-import sh.calvin.reorderable.ReorderableItem
-import sh.calvin.reorderable.rememberReorderableLazyListState
 
 
 @Composable
 @Composable
 fun FloatingBarIconConfigSheet(
 fun FloatingBarIconConfigSheet(
@@ -105,16 +74,7 @@ private fun ButtonIconConfigSheet(
     onClearIcon: (String) -> Unit,
     onClearIcon: (String) -> Unit,
 ) {
 ) {
     val context = LocalContext.current
     val context = LocalContext.current
-    var draftItems by remember(show, items) {
-        mutableStateOf(buildButtonIconEntries(items, context))
-    }
-
-    val lazyListState = rememberLazyListState()
-    val reorderableState = rememberReorderableLazyListState(lazyListState) { from, to ->
-        draftItems = draftItems.toMutableList().apply {
-            add(to.index, removeAt(from.index))
-        }
-    }
+    val entries = remember(items) { buildButtonIconEntries(items, context) }
 
 
     AppModalBottomSheet(
     AppModalBottomSheet(
         show = show,
         show = show,
@@ -126,209 +86,31 @@ private fun ButtonIconConfigSheet(
                 .fillMaxWidth()
                 .fillMaxWidth()
                 .padding(bottom = 16.dp),
                 .padding(bottom = 16.dp),
         ) {
         ) {
-            LazyColumn(
-                state = lazyListState,
-                verticalArrangement = Arrangement.spacedBy(4.dp),
-                modifier = Modifier.weight(1f, fill = false),
-            ) {
-                itemsIndexed(draftItems, key = { _, item -> item.id }) { index, item ->
-                    ReorderableItem(reorderableState, key = item.id) { isDragging ->
-                        val elevation by animateDpAsState(if (isDragging) 4.dp else 0.dp)
-                        NormalCard(
-                            elevation = elevation,
-                            cornerRadius = 12.dp,
-                            containerColor = LegadoTheme.colorScheme.surfaceContainerLow
-                        ) {
-                            ButtonIconConfigItem(
-                                item = item,
-                                customIcon = customIcons[item.id],
-                                onToggleEnabled = {
-                                    draftItems = draftItems.toggleButtonEnabled(item.id)
-                                },
-                                onSelectIcon = { onSelectIcon(item.id) },
-                                onClearIcon = { onClearIcon(item.id) },
-                                dragHandleModifier = Modifier
-                                    .reorderAccessibility(
-                                        index = index,
-                                        itemCount = draftItems.size,
-                                        description = stringResource(
-                                            R.string.a11y_reorder_named,
-                                            item.label,
-                                        ),
-                                    ) { from, to ->
-                                        draftItems = draftItems.toMutableList().apply {
-                                            add(to, removeAt(from))
-                                        }
-                                    }
-                                    .draggableHandle(),
-                            )
-                        }
-                    }
-                }
-            }
-
-            ConfirmDismissButtonsRow(
+            ReorderableConfigList(
+                initialEntries = entries,
+                customIcons = customIcons,
+                onSelectIcon = onSelectIcon,
+                onClearIcon = onClearIcon,
                 onDismiss = onDismissRequest,
                 onDismiss = onDismissRequest,
-                onConfirm = {
-                    onSaved(draftItems.map { ReadBookButtonConfigItem(it.id, it.enabled) })
+                onConfirm = { confirmed ->
+                    onSaved(confirmed.map { ReadBookButtonConfigItem(it.id, it.enabled) })
                     onDismissRequest()
                     onDismissRequest()
                 },
                 },
-                dismissText = stringResource(R.string.cancel),
-                confirmText = stringResource(R.string.action_save),
-                modifier = Modifier.padding(top = 8.dp),
+                reorderEnabledFirst = true,
             )
             )
         }
         }
     }
     }
 }
 }
 
 
-@Composable
-private fun ButtonIconConfigItem(
-    item: ButtonIconEntry,
-    customIcon: String?,
-    onToggleEnabled: () -> Unit,
-    onSelectIcon: () -> Unit,
-    onClearIcon: () -> Unit,
-    dragHandleModifier: Modifier = Modifier,
-) {
-    val icon = item.icon
-    val name = item.label
-    val alpha = if (item.enabled) 1f else 0.38f
-
-    Row(
-        verticalAlignment = Alignment.CenterVertically,
-        modifier = Modifier
-            .fillMaxWidth()
-            .padding(all = 12.dp),
-    ) {
-        Box(
-            contentAlignment = Alignment.Center,
-            modifier = Modifier.size(36.dp),
-        ) {
-            if (!customIcon.isNullOrBlank()) {
-                AsyncImage(
-                    model = customIcon,
-                    contentDescription = name,
-                    contentScale = ContentScale.Fit,
-                    modifier = Modifier.fillMaxSize(),
-                    alpha = alpha,
-                )
-            } else {
-                Icon(
-                    imageVector = icon,
-                    contentDescription = name,
-                    tint = LegadoTheme.colorScheme.onSurface.copy(alpha = alpha),
-                    modifier = Modifier.size(24.dp),
-                )
-            }
-        }
-
-        Text(
-            text = name,
-            style = LegadoTheme.typography.bodyLarge,
-            color = LegadoTheme.colorScheme.onSurface.copy(alpha = alpha),
-            maxLines = 1,
-            overflow = TextOverflow.Ellipsis,
-            modifier = Modifier
-                .weight(1f)
-                .padding(horizontal = 12.dp),
-        )
-
-        if (item.enabled) {
-            // Custom icon button
-            if (!customIcon.isNullOrBlank()) {
-                IconButton(
-                    onClick = onClearIcon,
-                    modifier = Modifier.size(36.dp),
-                ) {
-                    Icon(
-                        Icons.Default.Close,
-                        contentDescription = stringResource(R.string.delete),
-                        modifier = Modifier.size(18.dp),
-                    )
-                }
-            } else {
-                SmallTonalButton(
-                    onClick = onSelectIcon,
-                    icon = Icons.Default.Add,
-                    contentDescription = stringResource(R.string.add),
-                    modifier = Modifier.size(36.dp),
-                )
-            }
-        }
-
-        // Visibility toggle
-        IconButton(
-            onClick = onToggleEnabled,
-            modifier = Modifier.size(36.dp),
-        ) {
-            Icon(
-                imageVector = if (item.enabled) {
-                    Icons.Default.Visibility
-                } else {
-                    Icons.Default.VisibilityOff
-                },
-                contentDescription = stringResource(
-                    if (item.enabled) R.string.disable_selection else R.string.enable_selection
-                ),
-                tint = if (item.enabled) {
-                    LegadoTheme.colorScheme.onSurface
-                } else {
-                    LegadoTheme.colorScheme.onSurfaceVariant
-                },
-                modifier = Modifier.size(20.dp),
-            )
-        }
-
-        // Drag handle
-        Box(
-            modifier = dragHandleModifier.size(48.dp),
-            contentAlignment = Alignment.Center,
-        ) {
-            Icon(
-                Icons.Default.Menu,
-                contentDescription = null,
-                modifier = Modifier.size(20.dp),
-            )
-        }
-    }
-}
-
-private data class ButtonIconEntry(
-    val id: String,
-    val enabled: Boolean,
-    val icon: ImageVector,
-    val label: String,
-)
-
-private fun List<ButtonIconEntry>.toggleButtonEnabled(id: String): List<ButtonIconEntry> {
-    val index = indexOfFirst { it.id == id }
-    if (index < 0) return this
-
-    val target = this[index]
-    val toggled = target.copy(enabled = !target.enabled)
-    val remaining = toMutableList().apply { removeAt(index) }
-    val insertIndex = if (toggled.enabled) {
-        remaining.indexOfLast { it.enabled } + 1
-    } else {
-        remaining.indexOfFirst { !it.enabled }
-            .takeIf { it >= 0 }
-            ?: remaining.size
-    }
-
-    return remaining.apply {
-        add(insertIndex.coerceIn(0, size), toggled)
-    }
-}
-
 private fun buildButtonIconEntries(
 private fun buildButtonIconEntries(
     items: List<ReadBookButtonConfigItem>,
     items: List<ReadBookButtonConfigItem>,
     context: Context,
     context: Context,
-): List<ButtonIconEntry> {
+): List<ConfigListEntry> {
     val infoMap = readMenuButtonInfos(context).associateBy { it.id }
     val infoMap = readMenuButtonInfos(context).associateBy { it.id }
     return items.mapNotNull { item ->
     return items.mapNotNull { item ->
         val id = item.id
         val id = item.id
         infoMap[id]?.let { info ->
         infoMap[id]?.let { info ->
-            ButtonIconEntry(id, item.enabled, info.icon, info.label)
+            ConfigListEntry(id, item.enabled, info.icon, info.label)
         }
         }
     }
     }
 }
 }

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

@@ -23,10 +23,10 @@ import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.DarkMode
 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.GridView
 import androidx.compose.material.icons.filled.LightMode
 import androidx.compose.material.icons.filled.LightMode
-import androidx.compose.material.icons.filled.Tablet
+import androidx.compose.material.icons.filled.SpaceBar
+import androidx.compose.material.icons.filled.TextFields
 import androidx.compose.material.icons.filled.Visibility
 import androidx.compose.material.icons.filled.Visibility
 import androidx.compose.material3.Icon
 import androidx.compose.material3.Icon
 import androidx.compose.material3.PlainTooltip
 import androidx.compose.material3.PlainTooltip
@@ -44,7 +44,6 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.stringArrayResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
@@ -77,7 +76,8 @@ fun GlobalThemePage(
     onToggleDayNight: () -> Unit,
     onToggleDayNight: () -> Unit,
     eyeProtectionEnabled: Boolean,
     eyeProtectionEnabled: Boolean,
     onOpenBgTextConfig: (Int) -> Unit,
     onOpenBgTextConfig: (Int) -> Unit,
-    onOpenUnderlineConfig: () -> Unit,
+    onOpenTypographyConfig: () -> Unit,
+    onOpenPaddingConfig: () -> Unit,
     onShareLayoutChange: (Boolean) -> Unit,
     onShareLayoutChange: (Boolean) -> Unit,
     onStyleSelect: (Int) -> Unit,
     onStyleSelect: (Int) -> Unit,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
@@ -91,7 +91,6 @@ fun GlobalThemePage(
     val styleSelect = styleConfig.styleSelect
     val styleSelect = styleConfig.styleSelect
     val shareLayout = styleConfig.shareLayout
     val shareLayout = styleConfig.shareLayout
     val isNightTheme = LegadoTheme.isDark
     val isNightTheme = LegadoTheme.isDark
-    val doubleHorizontalPage = preferences.doubleHorizontalPage
 
 
     val configList = styleConfig.styleItems
     val configList = styleConfig.styleItems
 
 
@@ -115,7 +114,7 @@ fun GlobalThemePage(
                 },
                 },
             )
             )
             NormalCard(
             NormalCard(
-                onClick = onOpenUnderlineConfig,
+                onClick = onOpenTypographyConfig,
                 modifier = Modifier
                 modifier = Modifier
                     .height(56.dp)
                     .height(56.dp)
                     .aspectRatio(1f),
                     .aspectRatio(1f),
@@ -127,9 +126,9 @@ fun GlobalThemePage(
                     modifier = Modifier.fillMaxSize(),
                     modifier = Modifier.fillMaxSize(),
                 ) {
                 ) {
                     Icon(
                     Icon(
-                        imageVector = Icons.Default.FormatUnderlined,
-                        contentDescription = stringResource(R.string.use_underline),
-                        tint = LegadoTheme.colorScheme.onSurfaceVariant
+                        imageVector = Icons.Default.TextFields,
+                        contentDescription = stringResource(R.string.compose_type),
+                        tint = LegadoTheme.colorScheme.onSurfaceVariant,
                     )
                     )
                 }
                 }
             }
             }
@@ -323,10 +322,6 @@ fun GlobalThemePage(
         val currentPageAnimDisplay =
         val currentPageAnimDisplay =
             pageAnimEntries.getOrNull(pageAnimEntryValues.indexOf(pageAnim.toString())) ?: ""
             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(
         Row(
             modifier = Modifier.fillMaxWidth(),
             modifier = Modifier.fillMaxWidth(),
             horizontalArrangement = Arrangement.spacedBy(8.dp),
             horizontalArrangement = Arrangement.spacedBy(8.dp),
@@ -363,51 +358,20 @@ fun GlobalThemePage(
                     }
                     }
                 }
                 }
             }
             }
-            // 平板/横屏双页 — 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()
-                            },
-                        )
-                    }
+            NormalCard(
+                onClick = onOpenPaddingConfig,
+                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.SpaceBar,
+                        contentDescription = stringResource(R.string.padding),
+                        tint = LegadoTheme.colorScheme.onSurfaceVariant,
+                    )
                 }
                 }
             }
             }
         }
         }

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

@@ -87,6 +87,9 @@ fun MoreConfigSheet(
             SectionTitle(stringResource(R.string.page_control))
             SectionTitle(stringResource(R.string.page_control))
             PageControlSettings(
             PageControlSettings(
                 preferences = preferences,
                 preferences = preferences,
+                onDoubleHorizontalPageChange = {
+                    onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.DoubleHorizontalPage(it)))
+                },
                 onProgressBarBehaviorChange = {
                 onProgressBarBehaviorChange = {
                     onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ProgressBarBehavior(it)))
                     onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ProgressBarBehavior(it)))
                 },
                 },
@@ -226,15 +229,25 @@ private fun ScreenSettings(
 @Composable
 @Composable
 private fun PageControlSettings(
 private fun PageControlSettings(
     preferences: ReadPreferences,
     preferences: ReadPreferences,
+    onDoubleHorizontalPageChange: (String) -> Unit,
     onProgressBarBehaviorChange: (String) -> Unit,
     onProgressBarBehaviorChange: (String) -> Unit,
     onMouseWheelPageChange: (Boolean) -> Unit,
     onMouseWheelPageChange: (Boolean) -> Unit,
     onVolumeKeyPageChange: (Boolean) -> Unit,
     onVolumeKeyPageChange: (Boolean) -> Unit,
     onVolumeKeyPageOnPlayChange: (Boolean) -> Unit,
     onVolumeKeyPageOnPlayChange: (Boolean) -> Unit,
     onKeyPageOnLongPressChange: (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 progressBarEntries = stringArrayResource(R.array.progress_bar_behavior_title)
     val progressBarValues = stringArrayResource(R.array.progress_bar_behavior_value)
     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(
     TinyDropdownSettingItem(
         title = stringResource(R.string.progress_bar_behavior),
         title = stringResource(R.string.progress_bar_behavior),
         selectedValue = preferences.progressBarBehavior,
         selectedValue = preferences.progressBarBehavior,

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

@@ -39,9 +39,10 @@ import kotlinx.coroutines.launch
 @Composable
 @Composable
 fun ReadStyleContent(
 fun ReadStyleContent(
     onOpenTypographyConfig: () -> Unit,
     onOpenTypographyConfig: () -> Unit,
+    onOpenInformationConfig: () -> Unit,
+    onOpenPaddingConfig: () -> Unit,
     onOpenMoreConfig: () -> Unit,
     onOpenMoreConfig: () -> Unit,
     onOpenBgTextConfig: (Int) -> Unit,
     onOpenBgTextConfig: (Int) -> Unit,
-    onOpenUnderlineConfig: () -> Unit,
     onOpenFontSelect: () -> Unit,
     onOpenFontSelect: () -> Unit,
     onToggleDayNight: () -> Unit,
     onToggleDayNight: () -> Unit,
     onPageChanged: (Int) -> Unit = {},
     onPageChanged: (Int) -> Unit = {},
@@ -96,7 +97,8 @@ fun ReadStyleContent(
                         onToggleDayNight = onToggleDayNight,
                         onToggleDayNight = onToggleDayNight,
                         eyeProtectionEnabled = eyeProtectionEnabled,
                         eyeProtectionEnabled = eyeProtectionEnabled,
                         onOpenBgTextConfig = onOpenBgTextConfig,
                         onOpenBgTextConfig = onOpenBgTextConfig,
-                        onOpenUnderlineConfig = onOpenUnderlineConfig,
+                        onOpenTypographyConfig = onOpenTypographyConfig,
+                        onOpenPaddingConfig = onOpenPaddingConfig,
                         onShareLayoutChange = { shareLayout ->
                         onShareLayoutChange = { shareLayout ->
                             onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShareLayout(shareLayout)))
                             onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShareLayout(shareLayout)))
                         },
                         },
@@ -123,7 +125,7 @@ fun ReadStyleContent(
         val tabTitles = listOf(
         val tabTitles = listOf(
             stringResource(R.string.read_config_global_theme),
             stringResource(R.string.read_config_global_theme),
             stringResource(R.string.read_config_menu_system),
             stringResource(R.string.read_config_menu_system),
-            stringResource(R.string.compose_type),
+            stringResource(R.string.information),
             stringResource(R.string.more_setting),
             stringResource(R.string.more_setting),
         )
         )
         CardTabRow(
         CardTabRow(
@@ -139,7 +141,7 @@ fun ReadStyleContent(
                             )
                             )
                         }
                         }
                     }
                     }
-                    2 -> onOpenTypographyConfig()
+                    2 -> onOpenInformationConfig()
                     3 -> onOpenMoreConfig()
                     3 -> onOpenMoreConfig()
                 }
                 }
             },
             },

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

@@ -4,20 +4,18 @@ import android.app.Activity
 import android.content.Intent
 import android.content.Intent
 import android.net.Uri
 import android.net.Uri
 import android.widget.Toast
 import android.widget.Toast
-import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.compose.BackHandler
 import androidx.activity.compose.BackHandler
+import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.background
 import androidx.compose.foundation.background
 import androidx.compose.foundation.basicMarquee
 import androidx.compose.foundation.basicMarquee
 import androidx.compose.foundation.clickable
 import androidx.compose.foundation.clickable
-import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.height
@@ -32,9 +30,9 @@ import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.pager.HorizontalPager
 import androidx.compose.foundation.pager.HorizontalPager
 import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.automirrored.filled.MenuOpen
 import androidx.compose.material.icons.automirrored.filled.MenuOpen
-import androidx.compose.material.icons.filled.AutoFixHigh
 import androidx.compose.material.icons.filled.BookmarkAdd
 import androidx.compose.material.icons.filled.BookmarkAdd
 import androidx.compose.material.icons.filled.CheckCircle
 import androidx.compose.material.icons.filled.CheckCircle
 import androidx.compose.material.icons.filled.Clear
 import androidx.compose.material.icons.filled.Clear
@@ -43,12 +41,10 @@ import androidx.compose.material.icons.filled.ExpandMore
 import androidx.compose.material.icons.filled.LocationOn
 import androidx.compose.material.icons.filled.LocationOn
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.Numbers
 import androidx.compose.material.icons.filled.Numbers
-import androidx.compose.material.icons.automirrored.filled.OpenInNew
+import androidx.compose.material.icons.filled.Refresh
 import androidx.compose.material.icons.filled.Search
 import androidx.compose.material.icons.filled.Search
 import androidx.compose.material.icons.filled.SelectAll
 import androidx.compose.material.icons.filled.SelectAll
 import androidx.compose.material.icons.filled.SwapVert
 import androidx.compose.material.icons.filled.SwapVert
-import androidx.compose.material.icons.filled.Refresh
-import androidx.compose.material.icons.filled.Shuffle
 import androidx.compose.material.icons.outlined.DownloadForOffline
 import androidx.compose.material.icons.outlined.DownloadForOffline
 import androidx.compose.material3.Icon
 import androidx.compose.material3.Icon
 import androidx.compose.material3.IconButton
 import androidx.compose.material3.IconButton
@@ -62,8 +58,8 @@ import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.rotate
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.rotate
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.graphics.vector.ImageVector
@@ -71,7 +67,6 @@ import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalWindowInfo
 import androidx.compose.ui.platform.LocalWindowInfo
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.unit.sp
@@ -80,17 +75,15 @@ import io.legado.app.R
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isLocal
+import io.legado.app.ui.book.toc.DownloadState
+import io.legado.app.ui.book.toc.TocActivity
+import io.legado.app.ui.book.toc.TocBookmarkItemUi
 import io.legado.app.ui.book.toc.TocEffect
 import io.legado.app.ui.book.toc.TocEffect
 import io.legado.app.ui.book.toc.TocIntent
 import io.legado.app.ui.book.toc.TocIntent
-import io.legado.app.ui.book.toc.TocBookmarkItemUi
 import io.legado.app.ui.book.toc.TocItemUi
 import io.legado.app.ui.book.toc.TocItemUi
 import io.legado.app.ui.book.toc.TocUiState
 import io.legado.app.ui.book.toc.TocUiState
 import io.legado.app.ui.book.toc.TocViewModel
 import io.legado.app.ui.book.toc.TocViewModel
-import io.legado.app.ui.book.toc.DownloadState
-import io.legado.app.ui.book.toc.TocActivity
 import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity
 import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity
-import io.legado.app.ui.replace.ReplaceEditRoute
-import io.legado.app.ui.replace.ReplaceRuleActivity
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.AppFloatingActionButtonMenu
 import io.legado.app.ui.widget.components.AppFloatingActionButtonMenu
 import io.legado.app.ui.widget.components.EmptyMessage
 import io.legado.app.ui.widget.components.EmptyMessage
@@ -102,7 +95,6 @@ import io.legado.app.ui.widget.components.image.cover.CoilBookCover
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
-import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
 import io.legado.app.ui.widget.components.progressIndicator.AppContainedLoadingIndicator
 import io.legado.app.ui.widget.components.progressIndicator.AppContainedLoadingIndicator
 import io.legado.app.ui.widget.components.tabRow.CardTabRow
 import io.legado.app.ui.widget.components.tabRow.CardTabRow
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.text.AppText
@@ -198,9 +190,6 @@ fun ReaderBookSheetRoute(
                 }
                 }
             }
             }
         },
         },
-        onOpenReplaceRule = { route ->
-            context.startActivity(ReplaceRuleActivity.startIntent(context, route))
-        },
         onEditLocalTocRule = { regex ->
         onEditLocalTocRule = { regex ->
             tocRegexLauncher.launch(
             tocRegexLauncher.launch(
                 Intent(context, TxtTocRuleActivity::class.java).putExtra("tocRegex", regex)
                 Intent(context, TxtTocRuleActivity::class.java).putExtra("tocRegex", regex)
@@ -223,7 +212,6 @@ private fun ReaderBookSheet(
     onChapterClick: (Int) -> Unit,
     onChapterClick: (Int) -> Unit,
     onBookmarkNavigate: (Int, Int) -> Unit,
     onBookmarkNavigate: (Int, Int) -> Unit,
     onOpenFullScreen: (ReaderBookSheetTab) -> Unit,
     onOpenFullScreen: (ReaderBookSheetTab) -> Unit,
-    onOpenReplaceRule: (ReplaceEditRoute?) -> Unit,
     onEditLocalTocRule: (String?) -> Unit,
     onEditLocalTocRule: (String?) -> Unit,
     onExportBookmarks: (isMarkdown: Boolean, fileName: String) -> Unit,
     onExportBookmarks: (isMarkdown: Boolean, fileName: String) -> Unit,
 ) {
 ) {
@@ -248,10 +236,13 @@ private fun ReaderBookSheet(
         show = show,
         show = show,
         onDismissRequest = onDismissRequest,
         onDismissRequest = onDismissRequest,
         animateContentSize = false,
         animateContentSize = false,
+        contentPaddingEnabled = false,
         modifier = Modifier
         modifier = Modifier
             .heightIn(max = maxHeight),
             .heightIn(max = maxHeight),
     ) {
     ) {
-        ReaderBookHeader(book = state.book)
+        Box(modifier = Modifier.padding(horizontal = 16.dp)) {
+            ReaderBookHeader(book = state.book)
+        }
         CardTabRow(
         CardTabRow(
             tabTitles = listOf(
             tabTitles = listOf(
                 stringResource(R.string.information),
                 stringResource(R.string.information),
@@ -266,7 +257,7 @@ private fun ReaderBookSheet(
             },
             },
             modifier = Modifier
             modifier = Modifier
                 .fillMaxWidth()
                 .fillMaxWidth()
-                .padding(top = 8.dp),
+                .padding(start = 16.dp, top = 8.dp, end = 16.dp),
             tabEndContent = { index ->
             tabEndContent = { index ->
                 Box(
                 Box(
                     modifier = Modifier
                     modifier = Modifier
@@ -274,9 +265,9 @@ private fun ReaderBookSheet(
                         .size(16.dp)
                         .size(16.dp)
                         .clip(RoundedCornerShape(6.dp))
                         .clip(RoundedCornerShape(6.dp))
                         .clickable {
                         .clickable {
-                        onDismissRequest()
-                        onOpenFullScreen(ReaderBookSheetTab.entries[index])
-                    },
+                            onDismissRequest()
+                            onOpenFullScreen(ReaderBookSheetTab.entries[index])
+                        },
                     contentAlignment = Alignment.Center,
                     contentAlignment = Alignment.Center,
                 ) {
                 ) {
                     Icon(
                     Icon(
@@ -293,24 +284,31 @@ private fun ReaderBookSheet(
                 .fillMaxWidth()
                 .fillMaxWidth()
                 .weight(1f),
                 .weight(1f),
         ) { page ->
         ) { page ->
-            when (ReaderBookSheetTab.entries[page]) {
-                ReaderBookSheetTab.Information -> ReaderBookInformation(
-                    book = state.book,
-                )
-                ReaderBookSheetTab.Toc -> ReaderBookTocPage(
-                    state = state,
-                    onIntent = onIntent,
-                    onChapterClick = onChapterClick,
-                    onOpenReplaceRule = onOpenReplaceRule,
-                    onEditLocalTocRule = onEditLocalTocRule,
-                )
-                ReaderBookSheetTab.Bookmarks -> ReaderBookBookmarksPage(
-                    state = state,
-                    onIntent = onIntent,
-                    onBookmarkNavigate = onBookmarkNavigate,
-                    onEditBookmark = { editingBookmark = it },
-                    onExportBookmarks = onExportBookmarks,
-                )
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
+            ) {
+                when (ReaderBookSheetTab.entries[page]) {
+                    ReaderBookSheetTab.Information -> ReaderBookInformation(
+                        book = state.book,
+                    )
+
+                    ReaderBookSheetTab.Toc -> ReaderBookTocPage(
+                        state = state,
+                        onIntent = onIntent,
+                        onChapterClick = onChapterClick,
+                        onEditLocalTocRule = onEditLocalTocRule,
+                    )
+
+                    ReaderBookSheetTab.Bookmarks -> ReaderBookBookmarksPage(
+                        state = state,
+                        onIntent = onIntent,
+                        onBookmarkNavigate = onBookmarkNavigate,
+                        onEditBookmark = { editingBookmark = it },
+                        onExportBookmarks = onExportBookmarks,
+                    )
+                }
             }
             }
         }
         }
     }
     }
@@ -332,7 +330,7 @@ private fun ReaderBookSheet(
 }
 }
 
 
 @Composable
 @Composable
-private fun ReaderBookHeader(book: Book?) {
+internal fun ReaderBookHeader(book: Book?) {
     val current = ((book?.durChapterIndex ?: -1) + 1).coerceAtLeast(0)
     val current = ((book?.durChapterIndex ?: -1) + 1).coerceAtLeast(0)
     val total = book?.totalChapterNum?.coerceAtLeast(0) ?: 0
     val total = book?.totalChapterNum?.coerceAtLeast(0) ?: 0
 
 
@@ -525,7 +523,6 @@ private fun ReaderBookTocPage(
     state: TocUiState,
     state: TocUiState,
     onIntent: (TocIntent) -> Unit,
     onIntent: (TocIntent) -> Unit,
     onChapterClick: (Int) -> Unit,
     onChapterClick: (Int) -> Unit,
-    onOpenReplaceRule: (ReplaceEditRoute?) -> Unit,
     onEditLocalTocRule: (String?) -> Unit,
     onEditLocalTocRule: (String?) -> Unit,
 ) {
 ) {
     val action = state.action
     val action = state.action
@@ -607,12 +604,6 @@ private fun ReaderBookTocPage(
                 contentDescription = stringResource(R.string.reverse_toc),
                 contentDescription = stringResource(R.string.reverse_toc),
                 onClick = { onIntent(TocIntent.ReverseToc) },
                 onClick = { onIntent(TocIntent.ReverseToc) },
             )
             )
-            CompactToolIconBox(
-                selected = action.useReplace,
-                icon = Icons.Default.Shuffle,
-                contentDescription = stringResource(R.string.use_replace_rule),
-                onClick = { onIntent(TocIntent.ToggleUseReplace) },
-            )
             Box {
             Box {
                 CompactToolIconBox(
                 CompactToolIconBox(
                     selected = menuExpanded,
                     selected = menuExpanded,
@@ -628,7 +619,6 @@ private fun ReaderBookTocPage(
                         state = state,
                         state = state,
                         onIntent = onIntent,
                         onIntent = onIntent,
                         onDismiss = { menuExpanded = false },
                         onDismiss = { menuExpanded = false },
-                        onOpenReplaceRule = onOpenReplaceRule,
                         onEditLocalTocRule = onEditLocalTocRule,
                         onEditLocalTocRule = onEditLocalTocRule,
                     )
                     )
                 }
                 }
@@ -889,7 +879,6 @@ private fun ReaderBookTocMenu(
     state: TocUiState,
     state: TocUiState,
     onIntent: (TocIntent) -> Unit,
     onIntent: (TocIntent) -> Unit,
     onDismiss: () -> Unit,
     onDismiss: () -> Unit,
-    onOpenReplaceRule: (ReplaceEditRoute?) -> Unit,
     onEditLocalTocRule: (String?) -> Unit,
     onEditLocalTocRule: (String?) -> Unit,
 ) {
 ) {
     val book = state.book
     val book = state.book
@@ -915,29 +904,6 @@ private fun ReaderBookTocMenu(
         text = stringResource(R.string.update_toc),
         text = stringResource(R.string.update_toc),
         onClick = { dispatch(TocIntent.UpdateToc) },
         onClick = { dispatch(TocIntent.UpdateToc) },
     )
     )
-    RoundDropdownMenuItem(
-        text = stringResource(R.string.replace_rule_title),
-        onClick = {
-            onDismiss()
-            onOpenReplaceRule(null)
-        },
-    )
-    RoundDropdownMenuItem(
-        text = stringResource(R.string.add_replace_rule),
-        onClick = {
-            onDismiss()
-            val scopes = listOfNotNull(book?.name, book?.origin).joinToString(";")
-            onOpenReplaceRule(
-                ReplaceEditRoute(
-                    id = -1,
-                    pattern = "",
-                    scope = scopes,
-                    isScopeTitle = true,
-                    isScopeContent = false,
-                )
-            )
-        },
-    )
     if (book?.isLocal == true) {
     if (book?.isLocal == true) {
         RoundDropdownMenuItem(
         RoundDropdownMenuItem(
             text = stringResource(R.string.local_book_toc_rule),
             text = stringResource(R.string.local_book_toc_rule),

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

@@ -0,0 +1,471 @@
+package io.legado.app.ui.book.read.sheet
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+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.fillMaxWidth
+import androidx.compose.foundation.layout.height
+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.shape.RoundedCornerShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Animation
+import androidx.compose.material.icons.filled.AutoStories
+import androidx.compose.material.icons.filled.Bookmark
+import androidx.compose.material.icons.filled.BugReport
+import androidx.compose.material.icons.filled.CleanHands
+import androidx.compose.material.icons.filled.CloudDownload
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.ExpandMore
+import androidx.compose.material.icons.filled.FindReplace
+import androidx.compose.material.icons.filled.Image
+import androidx.compose.material.icons.filled.Refresh
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material.icons.filled.SwapHoriz
+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.material3.HorizontalDivider
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import io.legado.app.R
+import io.legado.app.constant.PageAnim
+import io.legado.app.data.entities.Book
+import io.legado.app.model.ReadBook
+import io.legado.app.ui.book.read.MoreActionIds
+import io.legado.app.ui.book.read.ReadBookButtonConfigItem
+import io.legado.app.ui.book.read.ReadBookIntent
+import io.legado.app.ui.book.read.ReadBookSheet
+import io.legado.app.ui.book.read.ReadBookUiState
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.ConfigListEntry
+import io.legado.app.ui.widget.components.ReorderableConfigList
+import io.legado.app.ui.widget.components.icon.AppIcon
+import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
+import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
+import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
+import io.legado.app.ui.widget.components.text.AppText
+
+private data class MoreActionSpec(
+    val id: String,
+    val label: String,
+    val icon: ImageVector,
+    val applicable: Boolean = true,
+    val selected: Boolean = false,
+    val onClick: () -> Unit,
+)
+
+@Composable
+fun ReaderMoreActionsSheet(
+    show: Boolean,
+    state: ReadBookUiState,
+    onIntent: (ReadBookIntent) -> Unit,
+    onDismissRequest: () -> Unit,
+) {
+    var editing by remember(show) { mutableStateOf(false) }
+    fun dispatch(intent: ReadBookIntent) {
+        onDismissRequest()
+        onIntent(intent)
+    }
+
+    val specs = moreActionSpecs(state, onIntent, ::dispatch)
+    val config = state.menuConfig.moreActionItems.ifEmpty {
+        MoreActionIds.map { ReadBookButtonConfigItem(it, true) }
+    }
+
+    AppModalBottomSheet(
+        show = show,
+        onDismissRequest = onDismissRequest,
+        contentPaddingEnabled = false,
+    ) {
+        Box(modifier = Modifier.padding(horizontal = 16.dp)) {
+            ReaderBookHeader(book = state.book)
+        }
+        Spacer(modifier = Modifier.height(16.dp))
+        if (editing) {
+            MoreActionsConfigContent(
+                items = config,
+                specs = specs,
+                onCancel = { editing = false },
+                onSave = {
+                    onIntent(ReadBookIntent.SaveMoreActionsConfig(it))
+                    editing = false
+                },
+            )
+        } else {
+            MoreActionsPager(
+                actions = orderedVisibleActions(config, specs) + MoreActionSpec(
+                    id = "configure_more_actions",
+                    label = stringResource(R.string.more_actions_config),
+                    icon = Icons.Default.Settings,
+                    onClick = { editing = true },
+                ),
+                state = state,
+                onIntent = onIntent,
+                dispatch = ::dispatch,
+            )
+        }
+        Spacer(modifier = Modifier.height(8.dp))
+    }
+}
+
+private fun orderedVisibleActions(
+    config: List<ReadBookButtonConfigItem>,
+    specs: List<MoreActionSpec>,
+): List<MoreActionSpec> {
+    val specMap = specs.associateBy { it.id }
+    return config.mapNotNull { item ->
+        specMap[item.id]?.takeIf { item.enabled && it.applicable }
+    }
+}
+
+@Composable
+private fun MoreActionsPager(
+    actions: List<MoreActionSpec>,
+    state: ReadBookUiState,
+    onIntent: (ReadBookIntent) -> Unit,
+    dispatch: (ReadBookIntent) -> Unit,
+) {
+    val pages = actions.chunked(8).ifEmpty { listOf(emptyList()) }
+    val pagerState = rememberPagerState { pages.size }
+    BoxWithConstraints(modifier = Modifier.fillMaxWidth()) {
+        val itemSize = (maxWidth - 56.dp) / 4
+        val rowCount = if (actions.size > 4) 2 else 1
+        HorizontalPager(
+            state = pagerState,
+            modifier = Modifier
+                .fillMaxWidth()
+                .height(
+                    itemSize * rowCount + if (rowCount == 2) 24.dp else 16.dp
+                ),
+        ) { page ->
+            val pageItems = pages[page]
+            Column(
+                modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
+                verticalArrangement = Arrangement.spacedBy(8.dp),
+            ) {
+                pageItems.chunked(4).forEach { rowItems ->
+                    Row(
+                        modifier = Modifier.fillMaxWidth(),
+                        horizontalArrangement = Arrangement.spacedBy(8.dp),
+                    ) {
+                        rowItems.forEach { action ->
+                            ActionSquareHost(
+                                action = action,
+                                state = state,
+                                onIntent = onIntent,
+                                dispatch = dispatch,
+                                modifier = Modifier.weight(1f),
+                            )
+                        }
+                        repeat(4 - rowItems.size) { Spacer(Modifier.weight(1f)) }
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun ActionSquareHost(
+    action: MoreActionSpec,
+    state: ReadBookUiState,
+    onIntent: (ReadBookIntent) -> Unit,
+    dispatch: (ReadBookIntent) -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    var expanded by remember(action.id) { mutableStateOf(false) }
+    val hasMore = action.id == "change_source" || action.id == "refresh"
+    Box(modifier = modifier) {
+        SquareAction(
+            icon = action.icon,
+            text = action.label,
+            selected = action.selected,
+            hasMore = hasMore,
+            onClick = if (action.id == "image_style" || action.id == "page_anim") {
+                { expanded = true }
+            } else {
+                action.onClick
+            },
+            onMoreClick = { expanded = true },
+        )
+        when (action.id) {
+            "change_source" -> RoundDropdownMenu(
+                expanded = expanded,
+                onDismissRequest = { expanded = false },
+            ) { dismiss ->
+                RoundDropdownMenuItem(
+                    text = stringResource(R.string.change_origin),
+                    onClick = { dismiss(); dispatch(ReadBookIntent.MenuBookChangeSource) },
+                )
+                RoundDropdownMenuItem(
+                    text = stringResource(R.string.chapter_change_source),
+                    onClick = { dismiss(); dispatch(ReadBookIntent.MenuChapterChangeSource) },
+                )
+            }
+
+            "refresh" -> RoundDropdownMenu(
+                expanded = expanded,
+                onDismissRequest = { expanded = false },
+            ) { dismiss ->
+                listOf(
+                    R.string.menu_refresh_dur to ReadBookIntent.MenuRefreshDur,
+                    R.string.menu_refresh_after to ReadBookIntent.MenuRefreshAfter,
+                    R.string.menu_refresh_all to ReadBookIntent.MenuRefreshAll,
+                ).forEach { (label, intent) ->
+                    RoundDropdownMenuItem(
+                        text = stringResource(label),
+                        onClick = { dismiss(); dispatch(intent) },
+                    )
+                }
+            }
+
+            "image_style" -> ImageStyleDropdown(
+                expanded = expanded,
+                currentStyle = state.book?.getImageStyle() ?: Book.imgStyleDefault,
+                onDismissRequest = { expanded = false },
+                onIntent = onIntent,
+            )
+
+            "page_anim" -> PageAnimDropdown(
+                expanded = expanded,
+                currentPageAnim = state.book?.getPageAnim() ?: -1,
+                onDismissRequest = { expanded = false },
+                onIntent = onIntent,
+            )
+        }
+    }
+}
+
+@Composable
+private fun SquareAction(
+    icon: ImageVector,
+    text: String,
+    selected: Boolean,
+    hasMore: Boolean,
+    onClick: () -> Unit,
+    onMoreClick: () -> Unit,
+) {
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .aspectRatio(1f)
+            .clip(RoundedCornerShape(16.dp))
+            .background(
+                if (selected) LegadoTheme.colorScheme.secondaryContainer
+                else LegadoTheme.colorScheme.surfaceContainerLow
+            ),
+    ) {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .weight(1f)
+                .clickable(onClick = onClick),
+            horizontalAlignment = Alignment.CenterHorizontally,
+            verticalArrangement = Arrangement.Center,
+        ) {
+            AppIcon(
+                icon,
+                contentDescription = null,
+                modifier = Modifier.size(20.dp)
+            )
+
+            val space = if (hasMore) 6.dp else 8.dp
+            Spacer(
+                modifier = Modifier.height(space)
+            )
+
+            AppText(
+                text = text,
+                style = LegadoTheme.typography.labelSmall,
+                textAlign = TextAlign.Center,
+                maxLines = 1,
+                overflow = TextOverflow.Ellipsis,
+                modifier = Modifier.padding(horizontal = 4.dp),
+            )
+        }
+        if (hasMore) {
+            HorizontalDivider(color = LegadoTheme.colorScheme.surfaceContainer)
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(16.dp)
+                    .clickable(onClick = onMoreClick),
+                contentAlignment = Alignment.Center,
+            ) {
+                Icon(
+                    imageVector = Icons.Default.ExpandMore,
+                    contentDescription = stringResource(R.string.more_actions),
+                    modifier = Modifier.size(12.dp),
+                )
+            }
+        }
+    }
+}
+
+@Composable
+private fun MoreActionsConfigContent(
+    items: List<ReadBookButtonConfigItem>,
+    specs: List<MoreActionSpec>,
+    onCancel: () -> Unit,
+    onSave: (List<ReadBookButtonConfigItem>) -> Unit,
+) {
+    val meta = specs.associateBy { it.id }
+    val entries = items.mapNotNull { item ->
+        meta[item.id]?.let { spec ->
+            ConfigListEntry(item.id, item.enabled, spec.icon, spec.label)
+        }
+    }
+
+    ReorderableConfigList(
+        initialEntries = entries,
+        modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
+        onDismiss = onCancel,
+        onConfirm = { confirmed ->
+            onSave(confirmed.map { ReadBookButtonConfigItem(it.id, it.enabled) })
+        },
+    )
+}
+
+@Composable
+private fun moreActionSpecs(
+    state: ReadBookUiState,
+    onIntent: (ReadBookIntent) -> Unit,
+    dispatch: (ReadBookIntent) -> Unit,
+): List<MoreActionSpec> = listOf(
+    MoreActionSpec(
+        "change_source", stringResource(R.string.change_origin), Icons.Default.SwapHoriz,
+        applicable = !state.isLocalBook, onClick = { dispatch(ReadBookIntent.MenuChangeSource) }),
+    MoreActionSpec(
+        "refresh", stringResource(R.string.menu_refresh), Icons.Default.Refresh,
+        applicable = !state.isLocalBook, onClick = { dispatch(ReadBookIntent.MenuRefreshDur) }),
+    MoreActionSpec(
+        "download", stringResource(R.string.offline_cache), Icons.Default.CloudDownload,
+        applicable = !state.isLocalBook,
+        onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Download)) }),
+    MoreActionSpec(
+        "toc_rule", stringResource(R.string.txt_toc_rule), Icons.Default.Toc,
+        applicable = state.isLocalTxt, onClick = { dispatch(ReadBookIntent.MenuTocRegex) }),
+    MoreActionSpec(
+        "charset", stringResource(R.string.set_charset), Icons.Default.Translate,
+        applicable = state.isLocalBook,
+        onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.Charset)) }),
+    MoreActionSpec(
+        "edit_content", stringResource(R.string.edit_content), Icons.Default.Edit,
+        onClick = { dispatch(ReadBookIntent.OpenContentEdit) }),
+    MoreActionSpec(
+        "add_bookmark", stringResource(R.string.bookmark_add), Icons.Default.Bookmark,
+        onClick = { dispatch(ReadBookIntent.AddBookmark) }),
+    MoreActionSpec(
+        "text_processing", stringResource(R.string.text_processing), Icons.Default.FindReplace,
+        onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.TextProcessing)) }),
+    MoreActionSpec(
+        "reverse_content", stringResource(R.string.reverse_content), Icons.Default.SwapVert,
+        onClick = { dispatch(ReadBookIntent.MenuReverseContent) }),
+    MoreActionSpec(
+        "re_segment", stringResource(R.string.re_segment), Icons.Default.Toc,
+        selected = state.reSegment, onClick = { onIntent(ReadBookIntent.MenuReSegment) }),
+    MoreActionSpec(
+        "del_ruby", stringResource(R.string.del_ruby_tag), Icons.Default.CleanHands,
+        applicable = state.isEpub, selected = state.delRubyTag,
+        onClick = { onIntent(ReadBookIntent.MenuDelRubyTag) }),
+    MoreActionSpec(
+        "del_h", stringResource(R.string.del_h_tag), Icons.Default.CleanHands,
+        applicable = state.isEpub, selected = state.delHTag,
+        onClick = { onIntent(ReadBookIntent.MenuDelHTag) }),
+    MoreActionSpec(
+        "image_style", stringResource(R.string.image_style), Icons.Default.Image,
+        onClick = {}),
+    MoreActionSpec(
+        "page_anim", stringResource(R.string.book_page_anim), Icons.Default.Animation,
+        onClick = {}),
+    MoreActionSpec(
+        "simulated_reading", stringResource(R.string.simulated_reading), Icons.Default.AutoStories,
+        onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.SimulatedReading)) }),
+    MoreActionSpec(
+        "get_progress", stringResource(R.string.get_book_progress), Icons.Default.Sync,
+        applicable = state.isReadingProgressSyncConfigured,
+        onClick = { dispatch(ReadBookIntent.MenuGetProgress) }),
+    MoreActionSpec(
+        "cover_progress", stringResource(R.string.cover_book_progress), Icons.Default.Sync,
+        applicable = state.isReadingProgressSyncConfigured,
+        onClick = { dispatch(ReadBookIntent.MenuCoverProgress) }),
+    MoreActionSpec(
+        "log", stringResource(R.string.log), Icons.Default.BugReport,
+        onClick = { onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.AppLog)) }),
+)
+
+@Composable
+private fun ImageStyleDropdown(
+    expanded: Boolean,
+    currentStyle: String,
+    onDismissRequest: () -> Unit,
+    onIntent: (ReadBookIntent) -> Unit,
+) {
+    RoundDropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) { dismiss ->
+        listOf(
+            R.string.btn_default_s to Book.imgStyleDefault,
+            R.string.image_style_full to Book.imgStyleFull,
+            R.string.image_style_text to Book.imgStyleText,
+            R.string.image_style_single to Book.imgStyleSingle,
+        ).forEach { (label, style) ->
+            RoundDropdownMenuItem(
+                text = stringResource(label),
+                isSelected = currentStyle == style,
+                onClick = { dismiss(); onIntent(ReadBookIntent.MenuImageStyle(style)) },
+            )
+        }
+    }
+}
+
+@Composable
+private fun PageAnimDropdown(
+    expanded: Boolean,
+    currentPageAnim: Int,
+    onDismissRequest: () -> Unit,
+    onIntent: (ReadBookIntent) -> Unit,
+) {
+    val pageAnimOptions = listOf(
+        R.string.btn_default_s to -1,
+        R.string.page_anim_cover to PageAnim.coverPageAnim,
+        R.string.page_anim_slide to PageAnim.slidePageAnim,
+        R.string.page_anim_simulation to PageAnim.simulationPageAnim,
+        R.string.page_anim_scroll to PageAnim.scrollPageAnim,
+        R.string.page_anim_fade to PageAnim.fadePageAnim,
+        R.string.page_anim_none to PageAnim.noAnim,
+    )
+    RoundDropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) { dismiss ->
+        pageAnimOptions.forEach { (label, value) ->
+            RoundDropdownMenuItem(
+                text = stringResource(label),
+                isSelected = currentPageAnim == value,
+                onClick = {
+                    ReadBook.book?.setPageAnim(value)
+                    dismiss()
+                    onIntent(ReadBookIntent.PageAnimChanged)
+                },
+            )
+        }
+    }
+}

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

@@ -24,6 +24,7 @@ import androidx.compose.material.icons.filled.Bookmark
 import androidx.compose.material.icons.filled.Brightness6
 import androidx.compose.material.icons.filled.Brightness6
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.filled.Edit
 import androidx.compose.material.icons.filled.FindReplace
 import androidx.compose.material.icons.filled.FindReplace
+import androidx.compose.material.icons.filled.MoreVert
 import androidx.compose.material.icons.filled.PlayArrow
 import androidx.compose.material.icons.filled.PlayArrow
 import androidx.compose.material.icons.filled.RecordVoiceOver
 import androidx.compose.material.icons.filled.RecordVoiceOver
 import androidx.compose.material.icons.filled.Search
 import androidx.compose.material.icons.filled.Search
@@ -61,8 +62,8 @@ import io.legado.app.data.repository.ReadPreferences
 import io.legado.app.ui.book.read.ConfigUpdate
 import io.legado.app.ui.book.read.ConfigUpdate
 import io.legado.app.ui.book.read.ReadBookButtonConfigItem
 import io.legado.app.ui.book.read.ReadBookButtonConfigItem
 import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.book.read.ReadBookIntent
-import io.legado.app.ui.book.read.ReadBookStyleConfig
 import io.legado.app.ui.book.read.ReadBookSheet
 import io.legado.app.ui.book.read.ReadBookSheet
+import io.legado.app.ui.book.read.ReadBookStyleConfig
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.SectionTitle
 import io.legado.app.ui.widget.components.SectionTitle
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
@@ -892,11 +893,15 @@ internal fun readMenuButtonInfos(context: Context): List<ReadMenuButtonInfo> = l
     ReadMenuButtonInfo("eye_protection", Icons.Default.Visibility, context.getString(R.string.eye_protection)),
     ReadMenuButtonInfo("eye_protection", Icons.Default.Visibility, context.getString(R.string.eye_protection)),
     ReadMenuButtonInfo("prev_chapter", Icons.Default.SkipPrevious, context.getString(R.string.previous_chapter)),
     ReadMenuButtonInfo("prev_chapter", Icons.Default.SkipPrevious, context.getString(R.string.previous_chapter)),
     ReadMenuButtonInfo("next_chapter", Icons.Default.SkipNext, context.getString(R.string.next_chapter)),
     ReadMenuButtonInfo("next_chapter", Icons.Default.SkipNext, context.getString(R.string.next_chapter)),
-    ReadMenuButtonInfo("replace", Icons.Default.FindReplace, context.getString(R.string.replace_purify)),
+    ReadMenuButtonInfo(
+        "replace",
+        Icons.Default.FindReplace,
+        context.getString(R.string.text_processing)
+    ),
     ReadMenuButtonInfo(
     ReadMenuButtonInfo(
         "replace_badge",
         "replace_badge",
         Icons.Default.FindReplace,
         Icons.Default.FindReplace,
-        context.getString(R.string.replace_purify_badge)
+        context.getString(R.string.text_processing)
     ),
     ),
     ReadMenuButtonInfo("translate", Icons.Default.Translate, context.getString(R.string.translate)),
     ReadMenuButtonInfo("translate", Icons.Default.Translate, context.getString(R.string.translate)),
     ReadMenuButtonInfo(
     ReadMenuButtonInfo(
@@ -909,6 +914,11 @@ internal fun readMenuButtonInfos(context: Context): List<ReadMenuButtonInfo> = l
         Icons.Default.Edit,
         Icons.Default.Edit,
         context.getString(R.string.ai_text_rewrite)
         context.getString(R.string.ai_text_rewrite)
     ),
     ),
+    ReadMenuButtonInfo(
+        "more_actions",
+        Icons.Default.MoreVert,
+        context.getString(R.string.more_actions)
+    ),
 )
 )
 
 
 // ========== Color Swatch ==========
 // ========== Color Swatch ==========

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

@@ -0,0 +1,600 @@
+package io.legado.app.ui.book.read.sheet
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.MenuOpen
+import androidx.compose.material.icons.filled.Delete
+import androidx.compose.material.icons.filled.FilterList
+import androidx.compose.material.icons.filled.FindReplace
+import androidx.compose.material.icons.filled.Search
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material3.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalWindowInfo
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import io.legado.app.R
+import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookContentProcess
+import io.legado.app.data.entities.ReplaceRule
+import io.legado.app.domain.model.TextProcessAction
+import io.legado.app.ui.book.read.ContentProcessConfigUiState
+import io.legado.app.ui.book.read.ContentProcessItemUi
+import io.legado.app.ui.book.read.ReadBookIntent
+import io.legado.app.ui.book.read.ReplaceRuleItemUi
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.ConfigListEntry
+import io.legado.app.ui.widget.components.ConfigListEntryRow
+import io.legado.app.ui.widget.components.EmptyMessage
+import io.legado.app.ui.widget.components.alert.AppAlertDialog
+import io.legado.app.ui.widget.components.button.series.MediumToggleButton
+import io.legado.app.ui.widget.components.button.series.MediumTonalButton
+import io.legado.app.ui.widget.components.card.NormalCard
+import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
+import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
+import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
+import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
+import io.legado.app.ui.widget.components.reorderAccessibility
+import io.legado.app.ui.widget.components.tabRow.CardTabRow
+import io.legado.app.ui.widget.components.text.AppText
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.coroutines.launch
+import sh.calvin.reorderable.ReorderableItem
+import sh.calvin.reorderable.rememberReorderableLazyListState
+
+private enum class ReplaceFilter {
+    All,
+    Enabled,
+    Disabled,
+    Effective,
+}
+
+/** 一次拖动排序的落点信息:把 [draggedId] 移到 [anchorId] 的旁边([afterAnchor] 为 true 时在其后)。 */
+private class ReplaceMove(
+    val draggedId: Long,
+    val anchorId: Long,
+    val afterAnchor: Boolean,
+)
+
+/**
+ * 把列表第 [from] 项移到第 [to] 项位置,返回移动后的列表与落点信息。
+ * 仅操作当前显示(已筛选)的子列表,[from] 或 [to] 越界时返回 null。
+ */
+private fun moveInList(
+    list: List<ReplaceRuleItemUi>,
+    from: Int,
+    to: Int,
+): Pair<List<ReplaceRuleItemUi>, ReplaceMove>? {
+    if (from == to || from !in list.indices || to !in list.indices) return null
+    val moved = list[from]
+    val newList = list.toMutableList().apply { add(to, removeAt(from)) }
+    val anchorId = if (to > from) newList[to - 1].id else newList[to + 1].id
+    return newList to ReplaceMove(moved.id, anchorId, to > from)
+}
+
+@Composable
+fun TextProcessingSheet(
+    show: Boolean,
+    book: Book?,
+    allRules: ImmutableList<ReplaceRuleItemUi>,
+    effectiveRules: ImmutableList<ReplaceRule>,
+    replaceEnabled: Boolean,
+    contentProcessState: ContentProcessConfigUiState,
+    onIntent: (ReadBookIntent) -> Unit,
+    onDismissRequest: () -> Unit,
+) {
+    val pagerState = rememberPagerState { 2 }
+    val scope = rememberCoroutineScope()
+    var replaceQuery by rememberSaveable(show) { mutableStateOf("") }
+    var contentQuery by rememberSaveable(show) { mutableStateOf("") }
+    var replaceFilter by rememberSaveable(show) { mutableStateOf(ReplaceFilter.Effective) }
+
+    val maxHeight = with(LocalDensity.current) {
+        LocalWindowInfo.current.containerSize.height.toDp() * 0.72f
+    }
+    LaunchedEffect(show) {
+        if (show) pagerState.scrollToPage(0)
+    }
+
+    AppModalBottomSheet(
+        show = show,
+        onDismissRequest = onDismissRequest,
+        animateContentSize = false,
+        contentPaddingEnabled = false,
+        modifier = Modifier.heightIn(max = maxHeight),
+    ) {
+        Box(modifier = Modifier.padding(horizontal = 16.dp)) {
+            ReaderBookHeader(book = book)
+        }
+        CardTabRow(
+            tabTitles = listOf(
+                stringResource(R.string.replace_purify),
+                stringResource(R.string.content_processes),
+            ),
+            selectedTabIndex = pagerState.currentPage,
+            onTabSelected = { scope.launch { pagerState.animateScrollToPage(it) } },
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(start = 16.dp, top = 8.dp, end = 16.dp),
+            tabEndContent = { index ->
+                if (index == 0) {
+                    Box(
+                        modifier = Modifier
+                            .padding(start = 8.dp)
+                            .size(16.dp)
+                            .clip(RoundedCornerShape(6.dp))
+                            .clickable {
+                                onDismissRequest()
+                                onIntent(ReadBookIntent.MenuSettingReplace)
+                            },
+                        contentAlignment = Alignment.Center,
+                    ) {
+                        Icon(
+                            imageVector = Icons.AutoMirrored.Filled.MenuOpen,
+                            contentDescription = stringResource(R.string.open),
+                            modifier = Modifier.size(16.dp),
+                        )
+                    }
+                }
+            },
+        )
+        HorizontalPager(
+            state = pagerState,
+            modifier = Modifier
+                .fillMaxWidth()
+                .weight(1f),
+        ) { page ->
+            Box(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
+            ) {
+                if (page == 0) {
+                    ReplaceRulesPage(
+                        query = replaceQuery,
+                        onQueryChange = { replaceQuery = it },
+                        filter = replaceFilter,
+                        onFilterChange = { replaceFilter = it },
+                        allRules = allRules,
+                        effectiveRules = effectiveRules,
+                        replaceEnabled = replaceEnabled,
+                        onIntent = onIntent,
+                    )
+                } else {
+                    ContentProcessesPage(
+                        query = contentQuery,
+                        onQueryChange = { contentQuery = it },
+                        state = contentProcessState,
+                        onIntent = onIntent,
+                    )
+                }
+            }
+        }
+    }
+
+    val deletingItem = contentProcessState.deleteItem
+    AppAlertDialog(
+        show = deletingItem != null,
+        onDismissRequest = { onIntent(ReadBookIntent.DismissDeleteContentProcess) },
+        title = stringResource(R.string.delete),
+        text = stringResource(R.string.content_process_delete_confirm_simple),
+        confirmText = stringResource(R.string.ok),
+        onConfirm = { onIntent(ReadBookIntent.ConfirmDeleteContentProcess) },
+        dismissText = stringResource(R.string.cancel),
+        onDismiss = { onIntent(ReadBookIntent.DismissDeleteContentProcess) },
+    )
+}
+
+@Composable
+private fun ReplaceRulesPage(
+    query: String,
+    onQueryChange: (String) -> Unit,
+    filter: ReplaceFilter,
+    onFilterChange: (ReplaceFilter) -> Unit,
+    allRules: List<ReplaceRuleItemUi>,
+    effectiveRules: List<ReplaceRule>,
+    replaceEnabled: Boolean,
+    onIntent: (ReadBookIntent) -> Unit,
+) {
+    val effectiveIds = remember(effectiveRules) { effectiveRules.mapTo(hashSetOf()) { it.id } }
+    var rules by remember(allRules, effectiveIds, filter, query) {
+        mutableStateOf(
+            allRules.filter { rule ->
+                val matchesFilter = when (filter) {
+                    ReplaceFilter.All -> true
+                    ReplaceFilter.Enabled -> rule.enabled
+                    ReplaceFilter.Disabled -> !rule.enabled
+                    ReplaceFilter.Effective -> rule.id in effectiveIds
+                }
+                matchesFilter && rule.matches(query)
+            }
+        )
+    }
+    var filterExpanded by remember { mutableStateOf(false) }
+    var lastMove by remember { mutableStateOf<ReplaceMove?>(null) }
+    val listState = rememberLazyListState()
+    val reorderState = rememberReorderableLazyListState(listState) { from, to ->
+        moveInList(rules, from.index, to.index)?.let { (newRules, move) ->
+            rules = newRules
+            lastMove = move
+        }
+    }
+    LaunchedEffect(reorderState.isAnyItemDragging) {
+        if (!reorderState.isAnyItemDragging) {
+            lastMove?.let { move ->
+                onIntent(
+                    ReadBookIntent.MoveReplaceRule(move.draggedId, move.anchorId, move.afterAnchor)
+                )
+            }
+            lastMove = null
+        }
+    }
+
+    Column(modifier = Modifier.fillMaxSize()) {
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(vertical = 8.dp),
+            horizontalArrangement = Arrangement.spacedBy(8.dp),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            CompactTextProcessSearch(
+                query = query,
+                onQueryChange = onQueryChange,
+                modifier = Modifier.weight(1f),
+            )
+            CompactTextProcessTool(
+                selected = replaceEnabled,
+                icon = Icons.Default.FindReplace,
+                contentDescription = stringResource(R.string.replace_purify),
+                onClick = { onIntent(ReadBookIntent.ChangeReplaceRule(!replaceEnabled)) },
+            )
+            Box {
+                CompactTextProcessTool(
+                    selected = filter != ReplaceFilter.Effective,
+                    icon = Icons.Default.FilterList,
+                    contentDescription = stringResource(R.string.replace_filter),
+                    onClick = { filterExpanded = true },
+                )
+                RoundDropdownMenu(
+                    expanded = filterExpanded,
+                    onDismissRequest = { filterExpanded = false },
+                ) { dismiss ->
+                    ReplaceFilter.entries.forEach { option ->
+                        RoundDropdownMenuItem(
+                            text = stringResource(option.labelRes),
+                            isSelected = option == filter,
+                            onClick = {
+                                onFilterChange(option)
+                                dismiss()
+                            },
+                        )
+                    }
+                }
+            }
+        }
+        if (rules.isEmpty()) {
+            EmptyMessage(
+                message = stringResource(R.string.replace_filter_empty),
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(top = 24.dp),
+            )
+        } else {
+            LazyColumn(
+                state = listState,
+                modifier = Modifier.fillMaxSize(),
+                contentPadding = PaddingValues(bottom = 8.dp),
+                verticalArrangement = Arrangement.spacedBy(8.dp),
+            ) {
+                items(rules, key = { it.id }) { rule ->
+                    ReorderableItem(reorderState, key = rule.id) { _ ->
+                        ReplaceRuleItem(
+                            rule = rule,
+                            onIntent = onIntent,
+                            dragHandleModifier = Modifier
+                                .reorderAccessibility(
+                                    index = rules.indexOf(rule),
+                                    itemCount = rules.size,
+                                    description = stringResource(
+                                        R.string.a11y_reorder_named,
+                                        rule.name,
+                                    ),
+                                ) { from, to ->
+                                    moveInList(rules, from, to)?.let { (newRules, move) ->
+                                        rules = newRules
+                                        onIntent(
+                                            ReadBookIntent.MoveReplaceRule(
+                                                move.draggedId,
+                                                move.anchorId,
+                                                move.afterAnchor,
+                                            )
+                                        )
+                                    }
+                                }
+                                .draggableHandle(),
+                        )
+                    }
+                }
+            }
+        }
+    }
+}
+
+@Composable
+private fun ReplaceRuleItem(
+    rule: ReplaceRuleItemUi,
+    onIntent: (ReadBookIntent) -> Unit,
+    dragHandleModifier: Modifier = Modifier,
+) {
+    NormalCard(
+        onClick = { onIntent(ReadBookIntent.OpenReplaceEditor(rule.id, rule.pattern)) },
+        modifier = Modifier.fillMaxWidth(),
+        containerColor = LegadoTheme.colorScheme.onSheetContent,
+    ) {
+        ConfigListEntryRow(
+            entry = ConfigListEntry(
+                id = rule.id.toString(),
+                enabled = rule.enabled,
+                label = rule.displayNameGroup,
+                subtitle = buildString {
+                    append(rule.pattern)
+                    append(" → ")
+                    append(rule.replacement.ifEmpty { "∅" })
+                },
+            ),
+            onToggleEnabled = {
+                onIntent(ReadBookIntent.SetReplaceRuleEnabled(rule.id, !rule.enabled))
+            },
+            dragHandleModifier = dragHandleModifier,
+        )
+    }
+}
+
+@Composable
+private fun ContentProcessesPage(
+    query: String,
+    onQueryChange: (String) -> Unit,
+    state: ContentProcessConfigUiState,
+    onIntent: (ReadBookIntent) -> Unit,
+) {
+    var viewingItem by remember { mutableStateOf<ContentProcessItemUi?>(null) }
+    val items = remember(state.items, query) {
+        state.items.filter {
+            query.isBlank() || it.selectedText.contains(query, true) ||
+                    it.replacementText.contains(query, true)
+        }
+    }
+    Column(modifier = Modifier.fillMaxSize()) {
+        CompactTextProcessSearch(
+            query = query,
+            onQueryChange = onQueryChange,
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(vertical = 8.dp),
+        )
+        when {
+            state.isLoading -> Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
+                AppCircularProgressIndicator()
+            }
+
+            state.errorMessage != null -> AppText(
+                text = state.errorMessage,
+                color = LegadoTheme.colorScheme.error,
+            )
+
+            items.isEmpty() -> EmptyMessage(
+                message = stringResource(R.string.content_process_empty),
+                modifier = Modifier.fillMaxWidth(),
+            )
+
+            else -> LazyColumn(
+                modifier = Modifier.fillMaxSize(),
+                contentPadding = PaddingValues(bottom = 8.dp),
+                verticalArrangement = Arrangement.spacedBy(8.dp),
+            ) {
+                items(items, key = { it.id }) { item ->
+                    ContentProcessRow(
+                        item = item,
+                        onClick = { viewingItem = item },
+                        onIntent = onIntent,
+                    )
+                }
+            }
+        }
+    }
+
+    viewingItem?.let { item ->
+        AppAlertDialog(
+            data = item,
+            onDismissRequest = { viewingItem = null },
+            title = contentProcessTitle(item),
+            content = {
+                Column {
+                    AppText(stringResource(R.string.ai_text_clean_before))
+                    AppText(item.selectedText, modifier = Modifier.padding(bottom = 8.dp))
+                    AppText(stringResource(R.string.ai_text_clean_after))
+                    AppText(item.replacementText.ifEmpty { stringResource(R.string.ai_text_clean_delete) })
+                }
+            },
+            confirmText = stringResource(R.string.ok),
+            onConfirm = { viewingItem = null },
+        )
+    }
+}
+
+@Composable
+private fun ContentProcessRow(
+    item: ContentProcessItemUi,
+    onClick: () -> Unit,
+    onIntent: (ReadBookIntent) -> Unit,
+) {
+    NormalCard(
+        onClick = onClick,
+        modifier = Modifier.fillMaxWidth(),
+        containerColor = LegadoTheme.colorScheme.onSheetContent,
+    ) {
+        Row(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(12.dp),
+            horizontalArrangement = Arrangement.spacedBy(8.dp),
+            verticalAlignment = Alignment.CenterVertically,
+        ) {
+            Column(modifier = Modifier.weight(1f)) {
+                AppText(
+                    text = contentProcessTitle(item),
+                    style = LegadoTheme.typography.labelLargeEmphasized,
+                )
+                AppText(
+                    text = item.selectedText,
+                    maxLines = 2,
+                    overflow = TextOverflow.Ellipsis,
+                    color = LegadoTheme.colorScheme.onSurfaceVariant,
+                )
+            }
+            MediumToggleButton(
+                checked = item.enabled,
+                onCheckedChange = {
+                    onIntent(ReadBookIntent.ToggleContentProcess(item.id, it))
+                },
+                icon = Icons.Default.VisibilityOff,
+                iconChecked = Icons.Default.Visibility,
+                contentDescription = stringResource(R.string.enable),
+            )
+            MediumTonalButton(
+                onClick = { onIntent(ReadBookIntent.RequestDeleteContentProcess(item)) },
+                icon = Icons.Default.Delete,
+                contentDescription = stringResource(R.string.delete),
+            )
+        }
+    }
+}
+
+@Composable
+private fun CompactTextProcessSearch(
+    query: String,
+    onQueryChange: (String) -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    val shape = RoundedCornerShape(10.dp)
+    BasicTextField(
+        value = query,
+        onValueChange = onQueryChange,
+        modifier = modifier
+            .height(32.dp)
+            .clip(shape)
+            .background(LegadoTheme.colorScheme.surfaceContainerLow),
+        singleLine = true,
+        textStyle = LegadoTheme.typography.bodySmall.copy(color = LegadoTheme.colorScheme.onSurface),
+        cursorBrush = SolidColor(LegadoTheme.colorScheme.primary),
+        decorationBox = { inner ->
+            Row(
+                modifier = Modifier
+                    .fillMaxSize()
+                    .padding(horizontal = 8.dp),
+                verticalAlignment = Alignment.CenterVertically,
+                horizontalArrangement = Arrangement.spacedBy(6.dp),
+            ) {
+                Icon(Icons.Default.Search, null, modifier = Modifier.size(14.dp))
+                Box(Modifier.weight(1f), contentAlignment = Alignment.CenterStart) {
+                    if (query.isEmpty()) AppText(
+                        text = stringResource(R.string.search),
+                        style = LegadoTheme.typography.labelSmall,
+                        color = LegadoTheme.colorScheme.onSurfaceVariant,
+                    )
+                    inner()
+                }
+            }
+        },
+    )
+}
+
+@Composable
+private fun CompactTextProcessTool(
+    selected: Boolean,
+    icon: androidx.compose.ui.graphics.vector.ImageVector,
+    contentDescription: String,
+    onClick: () -> Unit,
+) {
+    Box(
+        modifier = Modifier
+            .size(32.dp)
+            .clip(RoundedCornerShape(10.dp))
+            .background(
+                if (selected) LegadoTheme.colorScheme.secondaryContainer
+                else LegadoTheme.colorScheme.surfaceContainerLow
+            )
+            .clickable(onClick = onClick),
+        contentAlignment = Alignment.Center,
+    ) {
+        Icon(icon, contentDescription, modifier = Modifier.size(16.dp))
+    }
+}
+
+private fun ReplaceRuleItemUi.matches(query: String): Boolean = query.isBlank() ||
+        name.contains(query, true) || pattern.contains(query, true) ||
+        replacement.contains(query, true) || group.orEmpty().contains(query, true)
+
+private val ReplaceRuleItemUi.displayNameGroup: String
+    get() = if (group.isNullOrBlank()) name else "$name ($group)"
+
+private val ReplaceFilter.labelRes: Int
+    get() = when (this) {
+        ReplaceFilter.All -> R.string.replace_filter_all
+        ReplaceFilter.Enabled -> R.string.replace_filter_enabled
+        ReplaceFilter.Disabled -> R.string.replace_filter_disabled
+        ReplaceFilter.Effective -> R.string.replace_filter_effective
+    }
+
+@Composable
+private fun contentProcessTitle(item: ContentProcessItemUi): String {
+    val kind = when (item.kind) {
+        BookContentProcess.KIND_AI_CLEAN -> stringResource(R.string.content_process_ai_clean)
+        BookContentProcess.KIND_AI_REWRITE -> stringResource(R.string.content_process_ai_rewrite)
+        BookContentProcess.KIND_USER_UNDERLINE -> stringResource(R.string.content_process_user_underline)
+        BookContentProcess.KIND_USER_HIGHLIGHT -> stringResource(R.string.content_process_user_highlight)
+        else -> item.kind
+    }
+    val action = when (item.actionType) {
+        TextProcessAction.TYPE_DELETE -> stringResource(R.string.content_process_delete_action)
+        TextProcessAction.TYPE_INSERT_BEFORE,
+        TextProcessAction.TYPE_INSERT_AFTER -> stringResource(R.string.content_process_insert_action)
+
+        else -> stringResource(R.string.content_process_replace_action)
+    }
+    return "$kind · $action"
+}

+ 43 - 142
app/src/main/java/io/legado/app/ui/book/read/sheet/TextSelectMenuConfigSheet.kt

@@ -22,7 +22,6 @@ import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.KeyboardArrowDown
 import androidx.compose.material.icons.filled.KeyboardArrowDown
-import androidx.compose.material.icons.filled.Menu
 import androidx.compose.material.icons.filled.Remove
 import androidx.compose.material.icons.filled.Remove
 import androidx.compose.material.icons.filled.Save
 import androidx.compose.material.icons.filled.Save
 import androidx.compose.material.icons.filled.VisibilityOff
 import androidx.compose.material.icons.filled.VisibilityOff
@@ -39,6 +38,7 @@ import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.rotate
 import androidx.compose.ui.draw.rotate
+import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.unit.sp
@@ -46,6 +46,8 @@ import coil.compose.AsyncImage
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.ui.book.read.ActionMenuItem
 import io.legado.app.ui.book.read.ActionMenuItem
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.ConfigListEntry
+import io.legado.app.ui.widget.components.ConfigListEntryRow
 import io.legado.app.ui.widget.components.button.series.MediumTonalButton
 import io.legado.app.ui.widget.components.button.series.MediumTonalButton
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
@@ -185,10 +187,12 @@ fun TextSelectMenuConfigSheet(
                                 containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
                                 containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
                                 modifier = Modifier.animateItem()
                                 modifier = Modifier.animateItem()
                             ) {
                             ) {
-                                PrimaryMenuItemRow(
+                                SelectMenuItemRow(
                                     item = item,
                                     item = item,
                                     showIcon = showSelectMenuIcon,
                                     showIcon = showSelectMenuIcon,
-                                    onMoveToFolded = {
+                                    groupActionIcon = Icons.Default.Remove,
+                                    groupActionContentDescription = stringResource(R.string.collapsed_menu),
+                                    onGroupAction = {
                                         draftItems = draftItems.map {
                                         draftItems = draftItems.map {
                                             if (it.uniqueId == item.uniqueId) it.copy(showState = 1) else it
                                             if (it.uniqueId == item.uniqueId) it.copy(showState = 1) else it
                                         }
                                         }
@@ -209,7 +213,7 @@ fun TextSelectMenuConfigSheet(
                                         ) { from, to ->
                                         ) { from, to ->
                                             moveWithinGroup(group1Items, from, to)
                                             moveWithinGroup(group1Items, from, to)
                                         }
                                         }
-                                        .draggableHandle()
+                                        .draggableHandle(),
                                 )
                                 )
                             }
                             }
                         }
                         }
@@ -237,10 +241,12 @@ fun TextSelectMenuConfigSheet(
                                 containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
                                 containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
                                 modifier = Modifier.animateItem()
                                 modifier = Modifier.animateItem()
                             ) {
                             ) {
-                                FoldedMenuItemRow(
+                                SelectMenuItemRow(
                                     item = item,
                                     item = item,
                                     showIcon = showSelectMenuIcon,
                                     showIcon = showSelectMenuIcon,
-                                    onMoveToPrimary = {
+                                    groupActionIcon = Icons.Default.Add,
+                                    groupActionContentDescription = stringResource(R.string.primary_menu),
+                                    onGroupAction = {
                                         draftItems = draftItems.map {
                                         draftItems = draftItems.map {
                                             if (it.uniqueId == item.uniqueId) it.copy(showState = 0) else it
                                             if (it.uniqueId == item.uniqueId) it.copy(showState = 0) else it
                                         }
                                         }
@@ -261,7 +267,7 @@ fun TextSelectMenuConfigSheet(
                                         ) { from, to ->
                                         ) { from, to ->
                                             moveWithinGroup(group2Items, from, to)
                                             moveWithinGroup(group2Items, from, to)
                                         }
                                         }
-                                        .draggableHandle()
+                                        .draggableHandle(),
                                 )
                                 )
                             }
                             }
                         }
                         }
@@ -320,153 +326,48 @@ fun TextSelectMenuConfigSheet(
 }
 }
 
 
 @Composable
 @Composable
-private fun PrimaryMenuItemRow(
+private fun SelectMenuItemRow(
     item: ActionMenuItem,
     item: ActionMenuItem,
     showIcon: Boolean,
     showIcon: Boolean,
-    onMoveToFolded: () -> Unit,
+    groupActionIcon: ImageVector,
+    groupActionContentDescription: String,
+    onGroupAction: () -> Unit,
     onMoveToHidden: () -> Unit,
     onMoveToHidden: () -> Unit,
-    dragHandleModifier: Modifier = Modifier
+    dragHandleModifier: Modifier = Modifier,
 ) {
 ) {
-    Row(
-        verticalAlignment = Alignment.CenterVertically,
-        modifier = Modifier
-            .fillMaxWidth()
-            .padding(all = 12.dp),
-    ) {
-        if (showIcon && item.iconDrawable != null) {
-            Box(
-                contentAlignment = Alignment.Center,
+    ConfigListEntryRow(
+        entry = ConfigListEntry(
+            id = item.uniqueId,
+            enabled = item.showState != 2,
+            label = item.title,
+        ),
+        customIcon = if (showIcon) item.iconDrawable else null,
+        trailingContent = {
+            IconButton(
+                onClick = onGroupAction,
                 modifier = Modifier.size(36.dp),
                 modifier = Modifier.size(36.dp),
             ) {
             ) {
-                AsyncImage(
-                    model = item.iconDrawable,
-                    contentDescription = item.title,
-                    modifier = Modifier.size(24.dp)
+                Icon(
+                    imageVector = groupActionIcon,
+                    contentDescription = groupActionContentDescription,
+                    tint = LegadoTheme.colorScheme.onSurfaceVariant,
+                    modifier = Modifier.size(20.dp),
                 )
                 )
             }
             }
-        }
-
-        AppText(
-            text = item.title,
-            fontSize = 14.sp,
-            color = LegadoTheme.colorScheme.onSurface,
-            modifier = Modifier
-                .weight(1f)
-                .padding(horizontal = 12.dp),
-            maxLines = 1
-        )
-
-        IconButton(
-            onClick = onMoveToFolded,
-            modifier = Modifier.size(36.dp),
-        ) {
-            Icon(
-                imageVector = Icons.Default.Remove,
-                contentDescription = stringResource(R.string.collapsed_menu),
-                tint = LegadoTheme.colorScheme.onSurfaceVariant,
-                modifier = Modifier.size(20.dp),
-            )
-        }
-
-        IconButton(
-            onClick = onMoveToHidden,
-            modifier = Modifier.size(36.dp),
-        ) {
-            Icon(
-                imageVector = Icons.Default.VisibilityOff,
-                contentDescription = stringResource(R.string.hidden_items),
-                tint = LegadoTheme.colorScheme.onSurfaceVariant,
-                modifier = Modifier.size(20.dp),
-            )
-        }
-
-        Box(
-            modifier = dragHandleModifier.size(48.dp),
-            contentAlignment = Alignment.Center
-        ) {
-            Icon(
-                imageVector = Icons.Default.Menu,
-                contentDescription = null,
-                tint = LegadoTheme.colorScheme.onSurfaceVariant,
-                modifier = Modifier.size(20.dp),
-            )
-        }
-    }
-}
-
-@Composable
-private fun FoldedMenuItemRow(
-    item: ActionMenuItem,
-    showIcon: Boolean,
-    onMoveToPrimary: () -> Unit,
-    onMoveToHidden: () -> Unit,
-    dragHandleModifier: Modifier = Modifier
-) {
-    Row(
-        verticalAlignment = Alignment.CenterVertically,
-        modifier = Modifier
-            .fillMaxWidth()
-            .padding(all = 12.dp),
-    ) {
-        if (showIcon && item.iconDrawable != null) {
-            Box(
-                contentAlignment = Alignment.Center,
+            IconButton(
+                onClick = onMoveToHidden,
                 modifier = Modifier.size(36.dp),
                 modifier = Modifier.size(36.dp),
             ) {
             ) {
-                AsyncImage(
-                    model = item.iconDrawable,
-                    contentDescription = item.title,
-                    modifier = Modifier.size(24.dp)
+                Icon(
+                    imageVector = Icons.Default.VisibilityOff,
+                    contentDescription = stringResource(R.string.hidden_items),
+                    tint = LegadoTheme.colorScheme.onSurfaceVariant,
+                    modifier = Modifier.size(20.dp),
                 )
                 )
             }
             }
-        }
-
-        AppText(
-            text = item.title,
-            fontSize = 14.sp,
-            color = LegadoTheme.colorScheme.onSurface,
-            modifier = Modifier
-                .weight(1f)
-                .padding(horizontal = 12.dp),
-            maxLines = 1
-        )
-
-        IconButton(
-            onClick = onMoveToPrimary,
-            modifier = Modifier.size(36.dp),
-        ) {
-            Icon(
-                imageVector = Icons.Default.Add,
-                contentDescription = stringResource(R.string.primary_menu),
-                tint = LegadoTheme.colorScheme.onSurfaceVariant,
-                modifier = Modifier.size(20.dp),
-            )
-        }
-
-        IconButton(
-            onClick = onMoveToHidden,
-            modifier = Modifier.size(36.dp),
-        ) {
-            Icon(
-                imageVector = Icons.Default.VisibilityOff,
-                contentDescription = stringResource(R.string.hidden_items),
-                tint = LegadoTheme.colorScheme.onSurfaceVariant,
-                modifier = Modifier.size(20.dp),
-            )
-        }
-
-        Box(
-            modifier = dragHandleModifier.size(48.dp),
-            contentAlignment = Alignment.Center
-        ) {
-            Icon(
-                imageVector = Icons.Default.Menu,
-                contentDescription = null,
-                tint = LegadoTheme.colorScheme.onSurfaceVariant,
-                modifier = Modifier.size(20.dp),
-            )
-        }
-    }
+        },
+        dragHandleModifier = dragHandleModifier,
+    )
 }
 }
 
 
 @Composable
 @Composable

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

@@ -5,10 +5,18 @@ import androidx.compose.animation.core.tween
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.pager.HorizontalPager
 import androidx.compose.foundation.pager.HorizontalPager
 import androidx.compose.foundation.pager.rememberPagerState
 import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.SpaceBar
+import androidx.compose.material3.Icon
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
@@ -30,6 +38,8 @@ import io.legado.app.constant.ReadTipType
 import io.legado.app.ui.book.read.ConfigUpdate
 import io.legado.app.ui.book.read.ConfigUpdate
 import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.book.read.ReadSheetConfigUiState
 import io.legado.app.ui.book.read.ReadSheetConfigUiState
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.pager.pagerHeight
 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.rememberPagerAnimatedHeight
 import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
 import io.legado.app.ui.widget.components.pager.rememberPagerFlingPassThroughConnection
@@ -37,9 +47,7 @@ import io.legado.app.ui.widget.components.tabRow.CardTabRow
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 
 
 /**
 /**
- * 排版配置页 — 统一的 5 Tab 排版设置入口。
- *
- * Tabs: 正文 / 标题 / 页眉 / 页脚 / 边距
+ * 阅读样式的分组配置页。
  *
  *
  * 模态弹窗(FontSelectSheet / ColorPickerSheet / CustomTipDialog)状态上抛至本层级,
  * 模态弹窗(FontSelectSheet / ColorPickerSheet / CustomTipDialog)状态上抛至本层级,
  * 在 HorizontalPager 之外渲染,避免 clipToBounds 裁剪问题。
  * 在 HorizontalPager 之外渲染,避免 clipToBounds 裁剪问题。
@@ -54,17 +62,30 @@ fun TypographyPage(
     onOpenUnderlineConfig: () -> Unit,
     onOpenUnderlineConfig: () -> Unit,
     onOpenHighlightRule: () -> Unit,
     onOpenHighlightRule: () -> Unit,
     sameTitleRemoved: Boolean = false,
     sameTitleRemoved: Boolean = false,
+    section: TypographySection = TypographySection.Typography,
+    onOpenPaddingConfig: (() -> Unit)? = null,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
 ) {
 ) {
     val scope = rememberCoroutineScope()
     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 })
+    val pages = when (section) {
+        TypographySection.Typography -> listOf(0, 1)
+        TypographySection.Information -> listOf(2, 3)
+        TypographySection.Padding -> listOf(4, 5, 6)
+    }
+    val tabTitles = pages.map { page ->
+        stringResource(
+            when (page) {
+                0 -> R.string.main_body
+                1 -> R.string.body_title
+                2 -> R.string.header
+                3 -> R.string.footer
+                4 -> R.string.main_body
+                5 -> R.string.header
+                else -> R.string.footer
+            }
+        )
+    }
+    val pagerState = rememberPagerState(pageCount = { pages.size })
     var selectedTab by remember { mutableIntStateOf(0) }
     var selectedTab by remember { mutableIntStateOf(0) }
 
 
     // Hoisted modal sheet state — rendered outside the pager to avoid clipToBounds clipping
     // Hoisted modal sheet state — rendered outside the pager to avoid clipToBounds clipping
@@ -135,19 +156,54 @@ fun TypographyPage(
     Column(
     Column(
         modifier = modifier.fillMaxWidth(),
         modifier = modifier.fillMaxWidth(),
     ) {
     ) {
-        CardTabRow(
-            tabTitles = tabTitles,
-            selectedTabIndex = selectedTab,
-            onTabSelected = { index ->
-                scope.launch {
-                    pagerState.animateScrollToPage(
-                        page = index,
-                        animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)
-                    )
+        if (pages.size > 1) {
+            Row(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(IntrinsicSize.Min)
+                    .padding(start = 16.dp, end = 16.dp, top = 8.dp),
+            ) {
+                CardTabRow(
+                    tabTitles = tabTitles,
+                    selectedTabIndex = selectedTab,
+                    onTabSelected = { index ->
+                        scope.launch {
+                            pagerState.animateScrollToPage(
+                                page = index,
+                                animationSpec = tween(
+                                    durationMillis = 300,
+                                    easing = FastOutSlowInEasing
+                                )
+                            )
+                        }
+                    },
+                    modifier = Modifier.weight(1f),
+                )
+                if (section != TypographySection.Padding && onOpenPaddingConfig != null) {
+                    NormalCard(
+                        containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+                        onClick = onOpenPaddingConfig,
+                        modifier = Modifier
+                            .padding(start = 8.dp)
+                            .fillMaxHeight()
+                            .aspectRatio(1f),
+                        cornerRadius = 12.dp,
+                    ) {
+                        Box(
+                            modifier = Modifier
+                                .fillMaxWidth()
+                                .fillMaxHeight(),
+                            contentAlignment = Alignment.Center,
+                        ) {
+                            Icon(
+                                imageVector = Icons.Default.SpaceBar,
+                                contentDescription = stringResource(R.string.padding),
+                            )
+                        }
+                    }
                 }
                 }
-            },
-            modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp),
-        )
+            }
+        }
 
 
         HorizontalPager(
         HorizontalPager(
             state = pagerState,
             state = pagerState,
@@ -157,12 +213,13 @@ fun TypographyPage(
                 .weight(1f, fill = false)
                 .weight(1f, fill = false)
                 .clipToBounds()
                 .clipToBounds()
                 .pagerHeight(animatedHeight),
                 .pagerHeight(animatedHeight),
-        ) { page ->
+        ) { pagerIndex ->
+            val page = pages[pagerIndex]
             Box(
             Box(
                 modifier = Modifier
                 modifier = Modifier
                     .fillMaxWidth()
                     .fillMaxWidth()
                     .onSizeChanged { size ->
                     .onSizeChanged { size ->
-                        pageHeights[page] = size.height
+                        pageHeights[pagerIndex] = size.height
                     }
                     }
             ) {
             ) {
                 when (page) {
                 when (page) {
@@ -228,11 +285,13 @@ fun TypographyPage(
                         modifier = Modifier.padding(horizontal = 16.dp),
                         modifier = Modifier.padding(horizontal = 16.dp),
                     )
                     )
 
 
-                    4 -> TypographyMarginTab(
+                    4, 5, 6 -> TypographyMarginTab(
                         config = config,
                         config = config,
                         onIntent = onIntent,
                         onIntent = onIntent,
+                        page = page - 4,
                         modifier = Modifier.padding(horizontal = 16.dp),
                         modifier = Modifier.padding(horizontal = 16.dp),
                     )
                     )
+
                 }
                 }
             }
             }
         }
         }
@@ -278,3 +337,9 @@ fun TypographyPage(
         )
         )
     }
     }
 }
 }
+
+enum class TypographySection {
+    Typography,
+    Information,
+    Padding,
+}

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

@@ -3,11 +3,8 @@ package io.legado.app.ui.book.read.sheet
 import android.content.Context
 import android.content.Context
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.height
@@ -31,7 +28,6 @@ import androidx.compose.material.icons.filled.FormatLineSpacing
 import androidx.compose.material.icons.filled.FormatSize
 import androidx.compose.material.icons.filled.FormatSize
 import androidx.compose.material.icons.filled.FormatUnderlined
 import androidx.compose.material.icons.filled.FormatUnderlined
 import androidx.compose.material.icons.filled.Height
 import androidx.compose.material.icons.filled.Height
-import androidx.compose.material.icons.filled.Image
 import androidx.compose.material.icons.filled.Layers
 import androidx.compose.material.icons.filled.Layers
 import androidx.compose.material.icons.filled.LineWeight
 import androidx.compose.material.icons.filled.LineWeight
 import androidx.compose.material.icons.filled.Minimize
 import androidx.compose.material.icons.filled.Minimize
@@ -53,7 +49,6 @@ import androidx.compose.runtime.mutableIntStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalContext
@@ -63,10 +58,8 @@ import androidx.compose.ui.unit.dp
 import androidx.core.net.toUri
 import androidx.core.net.toUri
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.R
-import io.legado.app.data.entities.Book
-import io.legado.app.data.repository.ReadSettingsRepository
 import io.legado.app.constant.ReadTipType
 import io.legado.app.constant.ReadTipType
-import io.legado.app.model.ReadBook
+import io.legado.app.data.repository.ReadSettingsRepository
 import io.legado.app.ui.book.read.ConfigUpdate
 import io.legado.app.ui.book.read.ConfigUpdate
 import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.book.read.ReadSheetConfigUiState
 import io.legado.app.ui.book.read.ReadSheetConfigUiState
@@ -81,13 +74,13 @@ 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.card.NormalCard
 import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
 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.dialog.CustomTipDialog
-import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.settingItem.TinyClickableSettingItem
 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.TinyColorModeSettingItem
 import io.legado.app.ui.widget.components.settingItem.TinyColorSettingItem
 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.TinyDropdownSettingItem
 import io.legado.app.ui.widget.components.settingItem.TinySliderSettingItem
 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.settingItem.TinySwitchSettingItem
+import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.utils.getCompatColor
 import io.legado.app.utils.getCompatColor
 import org.koin.compose.koinInject
 import org.koin.compose.koinInject
 import kotlin.math.roundToInt
 import kotlin.math.roundToInt
@@ -116,7 +109,8 @@ private fun FontWeightSetting(
     value: Int,
     value: Int,
     onValueChange: (Int) -> Unit,
     onValueChange: (Int) -> Unit,
 ) {
 ) {
-    var showVariableWeight by remember { mutableStateOf(false) }
+    // 0 常规 / 1 粗体 / 2 细体,其余 100..900 为自定义可变字重
+    val isCustom = value !in 0..2
     var sliderValue by remember(value) {
     var sliderValue by remember(value) {
         mutableFloatStateOf(
         mutableFloatStateOf(
             when (value) {
             when (value) {
@@ -130,66 +124,64 @@ private fun FontWeightSetting(
     val weightEntries = stringArrayResource(R.array.text_font_weight)
     val weightEntries = stringArrayResource(R.array.text_font_weight)
 
 
     Column {
     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"),
-                    imageVector = Icons.Default.LineWeight,
-                    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.fillMaxSize(), contentAlignment = Alignment.Center) {
-                    AppIcon(Icons.Default.Tune, stringResource(R.string.font_weight_text))
-                }
-            }
-        }
+        TinyDropdownSettingItem(
+            title = stringResource(R.string.font_weight_text),
+            selectedValue = if (isCustom) "-1" else value.toString(),
+            displayEntries = arrayOf(
+                weightEntries[2],
+                weightEntries[0],
+                weightEntries[1],
+                stringResource(R.string.custom),
+            ),
+            entryValues = arrayOf("2", "0", "1", "-1"),
+            imageVector = Icons.Default.LineWeight,
+            onValueChange = {
+                val v = it.toInt()
+                onValueChange(if (v == -1) sliderValue.toInt() else v)
+            },
+        )
 
 
-        AnimatedVisibility(visible = showVariableWeight) {
-            NormalCard(
+        AnimatedVisibility(visible = isCustom) {
+            Column(
                 modifier = Modifier
                 modifier = Modifier
                     .fillMaxWidth()
                     .fillMaxWidth()
-                    .padding(bottom = 4.dp)
-                    .height(56.dp),
-                containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
-                cornerRadius = 12.dp,
+                    .padding(bottom = 4.dp),
+                verticalArrangement = Arrangement.spacedBy(4.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),
-                        )
-                    },
+                AppText(
+                    text = stringResource(R.string.font_weight_custom_description),
+                    style = LegadoTheme.typography.labelSmallEmphasized,
+                    color = LegadoTheme.colorScheme.onSurfaceVariant,
                 )
                 )
+                NormalCard(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .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),
+                            )
+                        },
+                    )
+                }
             }
             }
         }
         }
     }
     }
@@ -283,7 +275,7 @@ internal fun TypographyBodyTab(
             .verticalScroll(rememberScrollState()),
             .verticalScroll(rememberScrollState()),
     ) {
     ) {
         // 字体组
         // 字体组
-        SectionTitle(title = stringResource(R.string.text_typeface))
+        SectionTitle(title = stringResource(R.string.text_font))
         TinyClickableSettingItem(
         TinyClickableSettingItem(
             title = stringResource(R.string.select_font),
             title = stringResource(R.string.select_font),
             imageVector = Icons.Default.TextFields,
             imageVector = Icons.Default.TextFields,
@@ -423,32 +415,6 @@ internal fun TypographyBodyTab(
             },
             },
         )
         )
 
 
-        // 图片组
-        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,
-            imageVector = Icons.Default.Image,
-            onValueChange = { style ->
-                onIntent(ReadBookIntent.MenuImageStyle(style))
-            },
-        )
-
-        Spacer(Modifier.height(8.dp))
     }
     }
 }
 }
 
 
@@ -488,24 +454,6 @@ internal fun TypographyTitleTab(
             .fillMaxWidth()
             .fillMaxWidth()
             .verticalScroll(rememberScrollState()),
             .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"),
-            imageVector = Icons.Default.Title,
-            onValueChange = {
-                titleMode = it.toInt()
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleMode(titleMode)))
-            },
-        )
-
         // 字体组
         // 字体组
         SectionTitle(title = stringResource(R.string.text_font))
         SectionTitle(title = stringResource(R.string.text_font))
         TinyClickableSettingItem(
         TinyClickableSettingItem(
@@ -546,8 +494,23 @@ internal fun TypographyTitleTab(
             },
             },
         )
         )
 
 
-        // 分段组
-        SectionTitle(title = stringResource(R.string.title_segment))
+        // 样式组(标题位置 + 分段 + 间距 + 去重)
+        SectionTitle(title = stringResource(R.string.style))
+        TinyDropdownSettingItem(
+            title = stringResource(R.string.title_position),
+            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"),
+            imageVector = Icons.Default.Title,
+            onValueChange = {
+                titleMode = it.toInt()
+                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleMode(titleMode)))
+            },
+        )
         TinyDropdownSettingItem(
         TinyDropdownSettingItem(
             title = stringResource(R.string.split_title_mode),
             title = stringResource(R.string.split_title_mode),
             selectedValue = titleSegType.toString(),
             selectedValue = titleSegType.toString(),
@@ -599,6 +562,15 @@ internal fun TypographyTitleTab(
             },
             },
         )
         )
 
 
+        TinySwitchSettingItem(
+            title = stringResource(R.string.same_title_removed),
+            checked = sameTitleRemoved,
+            imageVector = Icons.Default.CleanHands,
+            onCheckedChange = {
+                onIntent(ReadBookIntent.MenuSameTitleRemoved)
+            },
+        )
+
         // 间距组
         // 间距组
         SectionTitle(title = stringResource(R.string.title_spacing))
         SectionTitle(title = stringResource(R.string.title_spacing))
         TinySliderSettingItem(
         TinySliderSettingItem(
@@ -622,17 +594,6 @@ internal fun TypographyTitleTab(
             },
             },
         )
         )
 
 
-        // 去重组
-        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))
         Spacer(Modifier.height(8.dp))
     }
     }
 
 
@@ -732,6 +693,7 @@ internal fun TypographyHeaderTab(
         )
         )
         TinyColorSettingItem(
         TinyColorSettingItem(
             title = stringResource(R.string.tip_divider_color),
             title = stringResource(R.string.tip_divider_color),
+            description = stringResource(R.string.tip_divider_color_shared_desc),
             colorValue = when (config.tipDividerColor) {
             colorValue = when (config.tipDividerColor) {
                 -1 -> context.getCompatColor(R.color.divider)
                 -1 -> context.getCompatColor(R.color.divider)
                 0 -> config.textColorDay
                 0 -> config.textColorDay
@@ -741,8 +703,7 @@ internal fun TypographyHeaderTab(
             onClick = { onOpenColorPicker(TypographyColorTarget.Divider) },
             onClick = { onOpenColorPicker(TypographyColorTarget.Divider) },
         )
         )
 
 
-        // 字体组
-        SectionTitle(title = stringResource(R.string.text_font))
+        SectionTitle(title = stringResource(R.string.text_typeface))
         TinyClickableSettingItem(
         TinyClickableSettingItem(
             title = stringResource(R.string.select_font),
             title = stringResource(R.string.select_font),
             imageVector = Icons.Default.TextFields,
             imageVector = Icons.Default.TextFields,
@@ -846,6 +807,17 @@ internal fun TypographyFooterTab(
             imageVector = Icons.Default.Minimize,
             imageVector = Icons.Default.Minimize,
             onCheckedChange = { onShowFooterLineChange(it) },
             onCheckedChange = { onShowFooterLineChange(it) },
         )
         )
+        TinyColorSettingItem(
+            title = stringResource(R.string.tip_divider_color),
+            description = stringResource(R.string.tip_divider_color_shared_desc),
+            colorValue = when (config.tipDividerColor) {
+                -1 -> context.getCompatColor(R.color.divider)
+                0 -> config.textColorDay
+                else -> config.tipDividerColor
+            },
+            imageVector = Icons.Default.Palette,
+            onClick = { onOpenColorPicker(TypographyColorTarget.Divider) },
+        )
 
 
         // 字体组
         // 字体组
         SectionTitle(title = stringResource(R.string.text_font))
         SectionTitle(title = stringResource(R.string.text_font))
@@ -896,6 +868,7 @@ internal fun TypographyFooterTab(
 internal fun TypographyMarginTab(
 internal fun TypographyMarginTab(
     config: ReadSheetConfigUiState,
     config: ReadSheetConfigUiState,
     onIntent: (ReadBookIntent) -> Unit,
     onIntent: (ReadBookIntent) -> Unit,
+    page: Int,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
 ) {
 ) {
     // Body padding state
     // Body padding state
@@ -904,10 +877,6 @@ internal fun TypographyMarginTab(
     var paddingLeft by remember(config.paddingLeft) { mutableFloatStateOf(config.paddingLeft.toFloat()) }
     var paddingLeft by remember(config.paddingLeft) { mutableFloatStateOf(config.paddingLeft.toFloat()) }
     var paddingRight by remember(config.paddingRight) { mutableFloatStateOf(config.paddingRight.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
     // Header padding state
     var headerPaddingTop by remember(config.headerPaddingTop) { mutableFloatStateOf(config.headerPaddingTop.toFloat()) }
     var headerPaddingTop by remember(config.headerPaddingTop) { mutableFloatStateOf(config.headerPaddingTop.toFloat()) }
     var headerPaddingBottom by remember(config.headerPaddingBottom) { mutableFloatStateOf(config.headerPaddingBottom.toFloat()) }
     var headerPaddingBottom by remember(config.headerPaddingBottom) { mutableFloatStateOf(config.headerPaddingBottom.toFloat()) }
@@ -923,101 +892,74 @@ internal fun TypographyMarginTab(
     Column(
     Column(
         modifier = modifier
         modifier = modifier
             .fillMaxWidth()
             .fillMaxWidth()
-            .verticalScroll(rememberScrollState()),
+            .verticalScroll(rememberScrollState())
+            .padding(top = 8.dp),
     ) {
     ) {
-        // 正文边距
-        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,
-            imageVector = Icons.Default.VerticalAlignTop,
-            onValueChange = { value ->
-                titleTopSpacing = value.toInt()
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleTopSpacing(titleTopSpacing)))
-            },
-        )
-        TinySliderSettingItem(
-            title = stringResource(R.string.title_margin_bottom),
-            value = titleBottomSpacing.toFloat(),
-            valueRange = 0f..200f,
-            imageVector = Icons.Default.VerticalAlignBottom,
-            onValueChange = { value ->
-                titleBottomSpacing = value.toInt()
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TitleBottomSpacing(titleBottomSpacing)))
-            },
-        )
+        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())))
+                },
+            )
 
 
-        // 页眉边距
-        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())))
-            },
-        )
+            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())))
+                },
+            )
 
 
-        // 页脚边距
-        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())))
-            },
-        )
+            else -> 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))
     }
     }
 }
 }
 
 

+ 4 - 3
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleContract.kt

@@ -6,12 +6,13 @@ import androidx.compose.runtime.Stable
 import io.legado.app.data.entities.ReplaceRule
 import io.legado.app.data.entities.ReplaceRule
 import io.legado.app.ui.book.read.ContentProcessConfigUiState
 import io.legado.app.ui.book.read.ContentProcessConfigUiState
 import io.legado.app.ui.book.read.ContentProcessItemUi
 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.InteractionState
 import io.legado.app.ui.widget.components.list.ListUiState
 import io.legado.app.ui.widget.components.list.ListUiState
 import io.legado.app.ui.widget.components.list.SelectableItem
 import io.legado.app.ui.widget.components.list.SelectableItem
 import kotlinx.collections.immutable.ImmutableList
 import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.ImmutableSet
 import kotlinx.collections.immutable.persistentListOf
 import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.persistentSetOf
 
 
 @Immutable
 @Immutable
 data class ReplaceRuleItemUi(
 data class ReplaceRuleItemUi(
@@ -48,8 +49,8 @@ data class ReplaceRuleItemUi(
 
 
 @Stable
 @Stable
 data class ReplaceRuleUiState(
 data class ReplaceRuleUiState(
-    override val items: List<ReplaceRuleItemUi> = emptyList(),
-    override val selectedIds: Set<Long> = emptySet(),
+    override val items: ImmutableList<ReplaceRuleItemUi> = persistentListOf(),
+    override val selectedIds: ImmutableSet<Long> = persistentSetOf(),
     override val searchKey: String = "",
     override val searchKey: String = "",
     val sortMode: String = "desc",
     val sortMode: String = "desc",
     val selectedGroup: String? = null,
     val selectedGroup: String? = null,

+ 1 - 70
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleScreen.kt

@@ -1,28 +1,24 @@
 package io.legado.app.ui.replace
 package io.legado.app.ui.replace
 
 
 import android.content.ClipData
 import android.content.ClipData
-import androidx.appcompat.app.AppCompatActivity
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AppCompatActivity
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.ExperimentalFoundationApi
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxHeight
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.foundation.lazy.rememberLazyListState
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Add
-import androidx.compose.material3.AlertDialog
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
 import androidx.compose.material3.LoadingIndicator
 import androidx.compose.material3.LoadingIndicator
-import androidx.compose.material3.OutlinedButton
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.SnackbarHostState
 import androidx.compose.material3.SnackbarResult
 import androidx.compose.material3.SnackbarResult
-import androidx.compose.material3.TextButton
 import androidx.compose.material3.animateFloatingActionButton
 import androidx.compose.material3.animateFloatingActionButton
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
@@ -45,9 +41,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.base.BaseRuleEvent
 import io.legado.app.base.BaseRuleEvent
 import io.legado.app.data.entities.ReplaceRule
 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.adaptiveContentPadding
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.widget.components.ActionItem
 import io.legado.app.ui.widget.components.ActionItem
@@ -65,7 +58,6 @@ import io.legado.app.ui.widget.components.lazylist.FastScrollLazyColumn
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.rules.RuleListScaffold
 import io.legado.app.ui.widget.components.rules.RuleListScaffold
 import io.legado.app.ui.widget.components.tabRow.AppTabRow
 import io.legado.app.ui.widget.components.tabRow.AppTabRow
-import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.utils.showHelp
 import io.legado.app.utils.showHelp
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
@@ -363,23 +355,6 @@ fun ReplaceRuleScreen(
         },
         },
         snackbarHostState = snackbarHostState,
         snackbarHostState = snackbarHostState,
         dropDownMenuContent = { dismiss ->
         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(
             RoundDropdownMenuItem(
                 text = stringResource(R.string.import_str),
                 text = stringResource(R.string.import_str),
                 onClick = { showImportSheet = true; dismiss() }
                 onClick = { showImportSheet = true; dismiss() }
@@ -530,48 +505,4 @@ 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) },
-    )
 }
 }

+ 4 - 2
app/src/main/java/io/legado/app/ui/replace/ReplaceRuleViewModel.kt

@@ -31,6 +31,7 @@ import io.legado.app.utils.splitNotBlank
 import kotlinx.collections.immutable.ImmutableList
 import kotlinx.collections.immutable.ImmutableList
 import kotlinx.collections.immutable.persistentListOf
 import kotlinx.collections.immutable.persistentListOf
 import kotlinx.collections.immutable.toImmutableList
 import kotlinx.collections.immutable.toImmutableList
+import kotlinx.collections.immutable.toImmutableSet
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.Flow
@@ -185,6 +186,7 @@ class ReplaceRuleViewModel(
             }
             }
             is ReplaceRuleIntent.ToggleContentProcess -> viewModelScope.launch {
             is ReplaceRuleIntent.ToggleContentProcess -> viewModelScope.launch {
                 bookContentProcessGateway.setEnabled(intent.id, intent.enabled)
                 bookContentProcessGateway.setEnabled(intent.id, intent.enabled)
+                loadContentProcesses()
             }
             }
             is ReplaceRuleIntent.RequestDeleteContentProcess -> _bookState.update {
             is ReplaceRuleIntent.RequestDeleteContentProcess -> _bookState.update {
                 it.copy(contentProcessState = it.contentProcessState.copy(deleteItem = intent.item))
                 it.copy(contentProcessState = it.contentProcessState.copy(deleteItem = intent.item))
@@ -252,8 +254,8 @@ class ReplaceRuleViewModel(
         importState: BaseImportUiState<ReplaceRule>
         importState: BaseImportUiState<ReplaceRule>
     ): ReplaceRuleUiState {
     ): ReplaceRuleUiState {
         return ReplaceRuleUiState(
         return ReplaceRuleUiState(
-            items = items,
-            selectedIds = selectedIds,
+            items = items.toImmutableList(),
+            selectedIds = selectedIds.toImmutableSet(),
             searchKey = _searchKey.value,
             searchKey = _searchKey.value,
             sortMode = _sortMode.value,
             sortMode = _sortMode.value,
             selectedGroup = _group.value,
             selectedGroup = _group.value,

+ 272 - 0
app/src/main/java/io/legado/app/ui/widget/components/ReorderableConfigList.kt

@@ -0,0 +1,272 @@
+package io.legado.app.ui.widget.components
+
+import androidx.compose.animation.core.animateDpAsState
+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.RowScope
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Add
+import androidx.compose.material.icons.filled.Close
+import androidx.compose.material.icons.filled.Menu
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.material3.Icon
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import io.legado.app.R
+import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
+import io.legado.app.ui.widget.components.button.series.SmallOutlinedButton
+import io.legado.app.ui.widget.components.button.series.SmallPlainButton
+import io.legado.app.ui.widget.components.button.series.SmallTonalButton
+import io.legado.app.ui.widget.components.card.NormalCard
+import sh.calvin.reorderable.ReorderableItem
+import sh.calvin.reorderable.rememberReorderableLazyListState
+
+/**
+ * 可排序的按钮/图标配置列表:卡片行 + 拖拽排序 + 可见性开关,可选自定义图标。
+ * 阅读菜单的更多菜单、底栏、悬浮栏图标配置页共用。
+ *
+ * @param reorderEnabledFirst 切换启用状态时是否把启用的条目排到前面
+ * @param onSelectIcon / @param onClearIcon 提供时启用自定义图标编辑(非空才显示)
+ */
+@Immutable
+data class ConfigListEntry(
+    val id: String,
+    val enabled: Boolean,
+    val icon: ImageVector? = null,
+    val label: String,
+    val subtitle: String? = null,
+)
+
+@Composable
+fun ReorderableConfigList(
+    initialEntries: List<ConfigListEntry>,
+    modifier: Modifier = Modifier,
+    customIcons: Map<String, String> = emptyMap(),
+    onSelectIcon: ((String) -> Unit)? = null,
+    onClearIcon: ((String) -> Unit)? = null,
+    onDismiss: () -> Unit,
+    onConfirm: (List<ConfigListEntry>) -> Unit,
+    reorderEnabledFirst: Boolean = false,
+) {
+    var entries by remember(initialEntries) { mutableStateOf(initialEntries) }
+    val listState = rememberLazyListState()
+    val reorderState = rememberReorderableLazyListState(listState) { from, to ->
+        entries = entries.toMutableList().apply { add(to.index, removeAt(from.index)) }
+    }
+
+    Column(modifier = modifier.fillMaxWidth()) {
+        LazyColumn(
+            state = listState,
+            verticalArrangement = Arrangement.spacedBy(4.dp),
+            modifier = Modifier.weight(1f, fill = false),
+        ) {
+            itemsIndexed(entries, key = { _, item -> item.id }) { index, entry ->
+                ReorderableItem(reorderState, key = entry.id) { dragging ->
+                    val elevation by animateDpAsState(if (dragging) 4.dp else 0.dp)
+                    NormalCard(
+                        elevation = elevation,
+                        cornerRadius = 12.dp,
+                        containerColor = LegadoTheme.colorScheme.surfaceContainerLow,
+                    ) {
+                        ConfigListEntryRow(
+                            entry = entry,
+                            customIcon = customIcons[entry.id],
+                            onToggleEnabled = {
+                                entries = entries.toggleEnabled(entry.id, reorderEnabledFirst)
+                            },
+                            onSelectIcon = onSelectIcon?.let { { it(entry.id) } },
+                            onClearIcon = onClearIcon?.let { { it(entry.id) } },
+                            dragHandleModifier = Modifier
+                                .reorderAccessibility(
+                                    index = index,
+                                    itemCount = entries.size,
+                                    description = stringResource(
+                                        R.string.a11y_reorder_named,
+                                        entry.label,
+                                    ),
+                                ) { from, to ->
+                                    entries = entries.toMutableList().apply {
+                                        add(to, removeAt(from))
+                                    }
+                                }
+                                .draggableHandle(),
+                        )
+                    }
+                }
+            }
+        }
+
+        ConfirmDismissButtonsRow(
+            onDismiss = onDismiss,
+            onConfirm = { onConfirm(entries) },
+            dismissText = stringResource(R.string.cancel),
+            confirmText = stringResource(R.string.action_save),
+            modifier = Modifier.padding(top = 8.dp),
+        )
+    }
+}
+
+@Composable
+fun ConfigListEntryRow(
+    entry: ConfigListEntry,
+    customIcon: Any? = null,
+    onToggleEnabled: (() -> Unit)? = null,
+    onSelectIcon: (() -> Unit)? = null,
+    onClearIcon: (() -> Unit)? = null,
+    trailingContent: (@Composable RowScope.() -> Unit)? = null,
+    dragHandleModifier: Modifier = Modifier,
+) {
+    val alpha = if (entry.enabled) 1f else 0.38f
+    val hasCustomIcon = customIcon?.let { if (it is String) it.isNotBlank() else true } ?: false
+
+    Row(
+        verticalAlignment = Alignment.CenterVertically,
+        horizontalArrangement = Arrangement.spacedBy(4.dp),
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(horizontal = 12.dp, vertical = 8.dp),
+    ) {
+        if (hasCustomIcon || entry.icon != null) {
+            Box(
+                contentAlignment = Alignment.Center,
+                modifier = Modifier.size(30.dp),
+            ) {
+                if (hasCustomIcon) {
+                    AsyncImage(
+                        model = customIcon,
+                        contentDescription = entry.label,
+                        contentScale = ContentScale.Fit,
+                        modifier = Modifier.fillMaxSize(),
+                        alpha = alpha,
+                    )
+                } else {
+                    entry.icon?.let { icon ->
+                        Icon(
+                            imageVector = icon,
+                            contentDescription = entry.label,
+                            tint = LegadoTheme.colorScheme.onSurface.copy(alpha = alpha),
+                            modifier = Modifier.size(24.dp),
+                        )
+                    }
+                }
+            }
+        }
+
+        Column(
+            modifier = Modifier
+                .weight(1f)
+                .padding(end = 12.dp),
+        ) {
+            Text(
+                text = entry.label,
+                style = LegadoTheme.typography.labelMediumEmphasized,
+                color = LegadoTheme.colorScheme.onSurface.copy(alpha = alpha),
+                maxLines = 1,
+                overflow = TextOverflow.Ellipsis,
+                modifier = Modifier.fillMaxWidth(),
+            )
+            if (!entry.subtitle.isNullOrBlank()) {
+                Text(
+                    text = entry.subtitle,
+                    style = LegadoTheme.typography.bodySmall,
+                    color = LegadoTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha),
+                    maxLines = 1,
+                    overflow = TextOverflow.Ellipsis,
+                    modifier = Modifier.fillMaxWidth(),
+                )
+            }
+        }
+
+        if (trailingContent != null) {
+            trailingContent()
+        } else {
+            // 自定义图标编辑(仅在宿主提供回调时显示)
+            if (entry.enabled && onSelectIcon != null && onClearIcon != null) {
+                if (hasCustomIcon) {
+                    SmallPlainButton(
+                        onClick = onClearIcon,
+                        icon = Icons.Default.Close,
+                        contentDescription = stringResource(R.string.delete)
+                    )
+                } else {
+                    SmallTonalButton(
+                        onClick = onSelectIcon,
+                        icon = Icons.Default.Add,
+                        contentDescription = stringResource(R.string.add),
+                    )
+                }
+            }
+
+            onToggleEnabled?.let { toggle ->
+                SmallOutlinedButton(
+                    onClick = toggle,
+                    icon = if (entry.enabled) {
+                        Icons.Default.Visibility
+                    } else {
+                        Icons.Default.VisibilityOff
+                    },
+                    contentDescription = stringResource(
+                        if (entry.enabled) R.string.disable_selection else R.string.enable_selection
+                    ),
+                )
+            }
+        }
+
+        Box(
+            modifier = dragHandleModifier.size(32.dp),
+            contentAlignment = Alignment.Center,
+        ) {
+            Icon(
+                Icons.Default.Menu,
+                contentDescription = null,
+                modifier = Modifier.size(24.dp),
+                tint = LegadoTheme.colorScheme.onSurfaceVariant
+            )
+        }
+    }
+}
+
+private fun List<ConfigListEntry>.toggleEnabled(
+    id: String,
+    reorderEnabledFirst: Boolean,
+): List<ConfigListEntry> {
+    val index = indexOfFirst { it.id == id }
+    if (index < 0) return this
+    val toggled = this[index].copy(enabled = !this[index].enabled)
+    if (!reorderEnabledFirst) {
+        return toMutableList().apply { set(index, toggled) }
+    }
+    val remaining = toMutableList().apply { removeAt(index) }
+    val insertIndex = if (toggled.enabled) {
+        remaining.indexOfLast { it.enabled } + 1
+    } else {
+        remaining.indexOfFirst { !it.enabled }
+            .takeIf { it >= 0 }
+            ?: remaining.size
+    }
+    return remaining.apply { add(insertIndex.coerceIn(0, size), toggled) }
+}

+ 2 - 12
app/src/main/java/io/legado/app/ui/widget/components/button/ToggleChip.kt

@@ -1,16 +1,10 @@
 package io.legado.app.ui.widget.components.button
 package io.legado.app.ui.widget.components.button
 
 
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.AnimatedVisibility
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.size
 import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.filled.Check
 import androidx.compose.material.icons.filled.Check
 import androidx.compose.material3.FilterChip
 import androidx.compose.material3.FilterChip
@@ -20,9 +14,6 @@ import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.semantics.Role
-import androidx.compose.ui.semantics.role
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.semantics
 import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.semantics.stateDescription
 import androidx.compose.ui.semantics.toggleableState
 import androidx.compose.ui.semantics.toggleableState
@@ -32,10 +23,9 @@ import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme.composeEngine
 import io.legado.app.ui.theme.LegadoTheme.composeEngine
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.card.NormalCard
+import top.yukonga.miuix.kmp.theme.MiuixTheme
 import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
 import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
-import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
 import top.yukonga.miuix.kmp.basic.Text as MiuixText
 import top.yukonga.miuix.kmp.basic.Text as MiuixText
-import top.yukonga.miuix.kmp.theme.MiuixTheme
 
 
 @Composable
 @Composable
 fun ToggleChip(
 fun ToggleChip(
@@ -96,7 +86,7 @@ fun ToggleChip(
                     style = LegadoTheme.typography.labelMediumEmphasized,
                     style = LegadoTheme.typography.labelMediumEmphasized,
                     maxLines = 1,
                     maxLines = 1,
                     softWrap = false,
                     softWrap = false,
-                    color = if (selected) MiuixTheme.colorScheme.onPrimary else MiuixTheme.colorScheme.onSurface
+                    color = if (selected) MiuixTheme.colorScheme.onSurface else MiuixTheme.colorScheme.onSurface
                 )
                 )
             }
             }
         }
         }

+ 17 - 3
app/src/main/java/io/legado/app/ui/widget/components/modalBottomSheet/AppModalBottomSheet.kt

@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.ColumnScope
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.heightIn
 import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.navigationBarsPadding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.padding
 import androidx.compose.material3.BottomSheetDefaults
 import androidx.compose.material3.BottomSheetDefaults
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3Api
@@ -52,6 +53,7 @@ fun AppModalBottomSheet(
     endAction: @Composable (() -> Unit)? = null,
     endAction: @Composable (() -> Unit)? = null,
     animateContentSize: Boolean = true,
     animateContentSize: Boolean = true,
     contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.modalWindowInsets },
     contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.modalWindowInsets },
+    contentPaddingEnabled: Boolean = true,
     content: @Composable ColumnScope.() -> Unit
     content: @Composable ColumnScope.() -> Unit
 ) {
 ) {
     val colorScheme = LocalLegadoThemeColors.current.colorScheme
     val colorScheme = LocalLegadoThemeColors.current.colorScheme
@@ -82,7 +84,7 @@ fun AppModalBottomSheet(
                     }
                     }
                 }
                 }
             },
             },
-            insideMargin = DpSize(16.dp, 0.dp),
+            insideMargin = if (contentPaddingEnabled) DpSize(16.dp, 0.dp) else DpSize(0.dp, 0.dp),
             backgroundColor = sheetContainerColor,
             backgroundColor = sheetContainerColor,
             dragHandleColor = sheetDragHandleColor,
             dragHandleColor = sheetDragHandleColor,
             onDismissRequest = onDismissRequest,
             onDismissRequest = onDismissRequest,
@@ -94,7 +96,13 @@ fun AppModalBottomSheet(
                     Column(
                     Column(
                         modifier = Modifier
                         modifier = Modifier
                             .fillMaxWidth()
                             .fillMaxWidth()
-                            .padding(bottom = 24.dp)
+                            .let {
+                                if (contentPaddingEnabled) {
+                                    it.padding(bottom = 24.dp)
+                                } else {
+                                    it.navigationBarsPadding()
+                                }
+                            }
                             .let { contentModifier ->
                             .let { contentModifier ->
                                 if (animateContentSize) contentModifier.animateContentSize() else contentModifier
                                 if (animateContentSize) contentModifier.animateContentSize() else contentModifier
                             },
                             },
@@ -131,7 +139,13 @@ fun AppModalBottomSheet(
                     Column(
                     Column(
                         modifier = Modifier
                         modifier = Modifier
                             .fillMaxWidth()
                             .fillMaxWidth()
-                            .padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
+                            .let {
+                                if (contentPaddingEnabled) {
+                                    it.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
+                                } else {
+                                    it
+                                }
+                            }
                             .heightIn(max = maxHeight)
                             .heightIn(max = maxHeight)
                             .let { contentModifier ->
                             .let { contentModifier ->
                                 if (animateContentSize) contentModifier.animateContentSize() else contentModifier
                                 if (animateContentSize) contentModifier.animateContentSize() else contentModifier

+ 28 - 1
app/src/main/java/io/legado/app/ui/widget/components/settingItem/TinySettingItems.kt

@@ -33,11 +33,13 @@ import androidx.compose.material.icons.filled.Refresh
 import androidx.compose.material3.Icon
 import androidx.compose.material3.Icon
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableFloatStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.setValue
+import androidx.compose.runtime.staticCompositionLocalOf
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.draw.clip
@@ -70,6 +72,28 @@ import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.text.AppText
 
 
+/**
+ * 滑块拖拽状态:宿主(如阅读菜单)可在任意滑块拖动时据此降低自身透明度。
+ */
+@Stable
+class SliderDragState {
+    var isDragging by mutableStateOf(false)
+        private set
+
+    fun startDragging() {
+        isDragging = true
+    }
+
+    fun stopDragging() {
+        isDragging = false
+    }
+}
+
+/**
+ * 提供当前 [SliderDragState] 的 CompositionLocal;宿主不提供时为 null(不参与透明度联动)。
+ */
+val LocalSliderDragState = staticCompositionLocalOf<SliderDragState?> { null }
+
 @Composable
 @Composable
 fun TinySettingItem(
 fun TinySettingItem(
     title: String,
     title: String,
@@ -270,6 +294,7 @@ fun TinySliderSettingItem(
     var expanded by remember { mutableStateOf(false) }
     var expanded by remember { mutableStateOf(false) }
     var sliderValue by remember(value) { mutableFloatStateOf(value) }
     var sliderValue by remember(value) { mutableFloatStateOf(value) }
     var displayValue by remember(value) { mutableFloatStateOf(value) }
     var displayValue by remember(value) { mutableFloatStateOf(value) }
+    val dragState = LocalSliderDragState.current
 
 
     TinySettingItem(
     TinySettingItem(
         title = title,
         title = title,
@@ -298,9 +323,11 @@ fun TinySliderSettingItem(
                 onValueChange = {
                 onValueChange = {
                     sliderValue = it
                     sliderValue = it
                     displayValue = it
                     displayValue = it
+                    onValueChange(it)
+                    dragState?.startDragging()
                 },
                 },
                 onValueChangeFinished = {
                 onValueChangeFinished = {
-                    onValueChange(sliderValue)
+                    dragState?.stopDragging()
                 },
                 },
                 valueRange = valueRange,
                 valueRange = valueRange,
                 steps = steps,
                 steps = steps,

+ 14 - 4
app/src/main/res/values-zh-rCN/strings.xml

@@ -258,7 +258,7 @@
     <string name="mouse_wheel_page">鼠标滚轮翻页</string>
     <string name="mouse_wheel_page">鼠标滚轮翻页</string>
     <string name="click_turn_page">点击翻页</string>
     <string name="click_turn_page">点击翻页</string>
     <string name="page_anim">翻页动画</string>
     <string name="page_anim">翻页动画</string>
-    <string name="book_page_anim">翻页动画(本书)</string>
+    <string name="book_page_anim">本书翻页动画</string>
     <string name="keep_light">屏幕超时</string>
     <string name="keep_light">屏幕超时</string>
     <string name="back">返回</string>
     <string name="back">返回</string>
     <string name="menu">菜单</string>
     <string name="menu">菜单</string>
@@ -983,6 +983,7 @@
     <string name="prev_page">上一页</string>
     <string name="prev_page">上一页</string>
     <string name="non_action">无操作</string>
     <string name="non_action">无操作</string>
     <string name="body_title">标题</string>
     <string name="body_title">标题</string>
+    <string name="title_position">标题位置</string>
     <string name="show_hide">显示/隐藏</string>
     <string name="show_hide">显示/隐藏</string>
     <string name="header_footer">信息</string>
     <string name="header_footer">信息</string>
     <string name="rule_subscription">规则</string>
     <string name="rule_subscription">规则</string>
@@ -1158,6 +1159,7 @@
     <string name="scope_content">作用于正文</string>
     <string name="scope_content">作用于正文</string>
     <string name="join_qq_channel">加入 QQ 频道</string>
     <string name="join_qq_channel">加入 QQ 频道</string>
     <string name="qq_channel_summary">点击加入阅读 QQ 频道</string>
     <string name="qq_channel_summary">点击加入阅读 QQ 频道</string>
+    <string name="menu_refresh">刷新章节</string>
     <string name="menu_refresh_dur">刷新当前章节</string>
     <string name="menu_refresh_dur">刷新当前章节</string>
     <string name="menu_refresh_after">刷新之后章节</string>
     <string name="menu_refresh_after">刷新之后章节</string>
     <string name="menu_refresh_all">刷新全部章节</string>
     <string name="menu_refresh_all">刷新全部章节</string>
@@ -1288,6 +1290,7 @@
     <string name="show_last_update_time">显示上次更新时间</string>
     <string name="show_last_update_time">显示上次更新时间</string>
     <string name="refresh_list">刷新列表</string>
     <string name="refresh_list">刷新列表</string>
     <string name="tip_divider_color">分隔线</string>
     <string name="tip_divider_color">分隔线</string>
+    <string name="tip_divider_color_shared_desc">页眉与页脚共用</string>
     <string name="same_title_removed">移除重复标题</string>
     <string name="same_title_removed">移除重复标题</string>
     <string name="update_book_fail">更新失败</string>
     <string name="update_book_fail">更新失败</string>
     <string name="notification_permission_rationale">阅读需要发送通知来显示朗读控制和下载进度。</string>
     <string name="notification_permission_rationale">阅读需要发送通知来显示朗读控制和下载进度。</string>
@@ -1418,6 +1421,7 @@
     <string name="record_heap_dump_t">记录堆转储</string>
     <string name="record_heap_dump_t">记录堆转储</string>
     <string name="font_weight_text">字重</string>
     <string name="font_weight_text">字重</string>
     <string name="font_weight">字重</string>
     <string name="font_weight">字重</string>
+    <string name="font_weight_custom_description">自定义字重仅适用于可变字重的字体</string>
     <string name="keep_swipe_tip">继续滑动以加载下一章…</string>
     <string name="keep_swipe_tip">继续滑动以加载下一章…</string>
     <string name="enable_optimize_render">启用绘制优化</string>
     <string name="enable_optimize_render">启用绘制优化</string>
     <string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行,拒绝后不再弹出。如有需要可至高级设置中再次申请。</string>
     <string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行,拒绝后不再弹出。如有需要可至高级设置中再次申请。</string>
@@ -1588,6 +1592,7 @@
     <string name="welcome_book_folder_tip">设置本地书籍保存目录,后续也可在设置中修改。</string>
     <string name="welcome_book_folder_tip">设置本地书籍保存目录,后续也可在设置中修改。</string>
     <string name="welcome_book_folder_not_selected">未选择文件夹</string>
     <string name="welcome_book_folder_not_selected">未选择文件夹</string>
     <string name="config_btn">配置底部按钮</string>
     <string name="config_btn">配置底部按钮</string>
+    <string name="more_actions_config">配置更多菜单</string>
     <string name="compact_grid">紧凑网格</string>
     <string name="compact_grid">紧凑网格</string>
     <string name="cover_grid">封面网格</string>
     <string name="cover_grid">封面网格</string>
     <string name="end_chapter">结束章</string>
     <string name="end_chapter">结束章</string>
@@ -1762,7 +1767,7 @@
     <string name="save_to_local">保存至本地</string>
     <string name="save_to_local">保存至本地</string>
     <string name="select_operation">选择操作</string>
     <string name="select_operation">选择操作</string>
     <string name="text_accent_color">强调</string>
     <string name="text_accent_color">强调</string>
-    <string name="read_color">颜色样式</string>
+    <string name="read_color">颜色</string>
     <string name="reverse_volume_key_page">反转音量键翻页方向</string>
     <string name="reverse_volume_key_page">反转音量键翻页方向</string>
     <string name="palette_style">调色板样式</string>
     <string name="palette_style">调色板样式</string>
     <string name="material_version">颜色规格</string>
     <string name="material_version">颜色规格</string>
@@ -1809,9 +1814,7 @@
 	<string name="subtitle_scale">次行缩放</string>
 	<string name="subtitle_scale">次行缩放</string>
 	<string name="subtitle_margin">次行距离</string>
 	<string name="subtitle_margin">次行距离</string>
 	<string name="heading_spacing">标题段距</string>
 	<string name="heading_spacing">标题段距</string>
-	<string name="title_segment">分段</string>
 	<string name="title_spacing">间距</string>
 	<string name="title_spacing">间距</string>
-	<string name="title_dedup">去重</string>
     <string name="follow_read_background">跟随阅读背景</string>
     <string name="follow_read_background">跟随阅读背景</string>
     <string name="bookshelf_total_count">共%1$d本</string>
     <string name="bookshelf_total_count">共%1$d本</string>
     <string name="bookshelf_selected_count">已选%1$d本</string>
     <string name="bookshelf_selected_count">已选%1$d本</string>
@@ -2070,6 +2073,13 @@
     <string name="ai_rewrite_preset_dialogue_name">口语自然</string>
     <string name="ai_rewrite_preset_dialogue_name">口语自然</string>
     <string name="ai_rewrite_preset_dialogue_instruction">把所选文字改写得更像自然叙述或对话,保持角色意图、人物关系和剧情事实不变。</string>
     <string name="ai_rewrite_preset_dialogue_instruction">把所选文字改写得更像自然叙述或对话,保持角色意图、人物关系和剧情事实不变。</string>
     <string name="content_processes">正文处理</string>
     <string name="content_processes">正文处理</string>
+    <string name="text_processing">文本处理</string>
+    <string name="replace_filter_all">全部</string>
+    <string name="replace_filter">筛选替换规则</string>
+    <string name="replace_filter_enabled">已启用</string>
+    <string name="replace_filter_disabled">未启用</string>
+    <string name="replace_filter_effective">正在生效</string>
+    <string name="replace_filter_empty">暂无匹配的替换净化</string>
     <string name="content_process_empty">当前章节没有正文处理。</string>
     <string name="content_process_empty">当前章节没有正文处理。</string>
     <string name="content_process_delete_confirm">删除这条正文处理?\n\n%1$s</string>
     <string name="content_process_delete_confirm">删除这条正文处理?\n\n%1$s</string>
     <string name="content_process_delete_confirm_simple">删除这条正文处理?</string>
     <string name="content_process_delete_confirm_simple">删除这条正文处理?</string>

+ 8 - 2
app/src/main/res/values-zh-rHK/strings.xml

@@ -208,7 +208,7 @@
     <string name="mouse_wheel_page">鼠标滚动翻页</string>
     <string name="mouse_wheel_page">鼠标滚动翻页</string>
     <string name="click_turn_page">點擊翻頁</string>
     <string name="click_turn_page">點擊翻頁</string>
     <string name="page_anim">翻頁動畫</string>
     <string name="page_anim">翻頁動畫</string>
-    <string name="book_page_anim">翻頁動畫(本書)</string>
+    <string name="book_page_anim">本書翻頁動畫</string>
     <string name="keep_light">屏幕超時</string>
     <string name="keep_light">屏幕超時</string>
     <string name="back">返回</string>
     <string name="back">返回</string>
     <string name="menu">菜單</string>
     <string name="menu">菜單</string>
@@ -843,6 +843,7 @@
     <string name="prev_page">上一頁</string>
     <string name="prev_page">上一頁</string>
     <string name="non_action">無操作</string>
     <string name="non_action">無操作</string>
     <string name="body_title">標題</string>
     <string name="body_title">標題</string>
+    <string name="title_position">標題位置</string>
     <string name="show_hide">顯示/隱藏</string>
     <string name="show_hide">顯示/隱藏</string>
     <string name="header_footer">頁眉<![CDATA[&]]>頁腳</string>
     <string name="header_footer">頁眉<![CDATA[&]]>頁腳</string>
     <string name="rule_subscription">規則訂閱</string>
     <string name="rule_subscription">規則訂閱</string>
@@ -995,6 +996,7 @@
     <string name="scope_content">作用于正文</string>
     <string name="scope_content">作用于正文</string>
     <string name="join_qq_channel">加入QQ频道</string>
     <string name="join_qq_channel">加入QQ频道</string>
     <string name="qq_channel_summary">点击加入阅读QQ频道</string>
     <string name="qq_channel_summary">点击加入阅读QQ频道</string>
+    <string name="menu_refresh">刷新章节</string>
     <string name="menu_refresh_dur">刷新当前章节</string>
     <string name="menu_refresh_dur">刷新当前章节</string>
     <string name="menu_refresh_after">刷新之后章节</string>
     <string name="menu_refresh_after">刷新之后章节</string>
     <string name="menu_refresh_all">刷新全部章节</string>
     <string name="menu_refresh_all">刷新全部章节</string>
@@ -1117,6 +1119,7 @@
     <string name="show_last_update_time">显示上次更新时间</string>
     <string name="show_last_update_time">显示上次更新时间</string>
     <string name="refresh_list">刷新列表</string>
     <string name="refresh_list">刷新列表</string>
     <string name="tip_divider_color">分隔线颜色</string>
     <string name="tip_divider_color">分隔线颜色</string>
+    <string name="tip_divider_color_shared_desc">页眉与页脚共用</string>
     <string name="same_title_removed">移除重复标题</string>
     <string name="same_title_removed">移除重复标题</string>
     <string name="update_book_fail">更新失败</string>
     <string name="update_book_fail">更新失败</string>
     <string name="notification_permission_rationale">阅读需要发送通知来显示朗读控制和下载进度</string>
     <string name="notification_permission_rationale">阅读需要发送通知来显示朗读控制和下载进度</string>
@@ -1229,6 +1232,7 @@
     <string name="record_heap_dump_t">记录堆转储</string>
     <string name="record_heap_dump_t">记录堆转储</string>
     <string name="font_weight_text">字重</string>
     <string name="font_weight_text">字重</string>
     <string name="font_weight">字重</string>
     <string name="font_weight">字重</string>
+    <string name="font_weight_custom_description">自訂字重僅適用於可變字重的字體</string>
     <string name="keep_swipe_tip">继续滑动以加载下一章…</string>
     <string name="keep_swipe_tip">继续滑动以加载下一章…</string>
     <string name="enable_optimize_render">启用绘制优化</string>
     <string name="enable_optimize_render">启用绘制优化</string>
     <string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
     <string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
@@ -1301,6 +1305,7 @@
 	<string name="subtitle_scale">次行縮放</string>
 	<string name="subtitle_scale">次行縮放</string>
 	<string name="subtitle_margin">次行距離</string>
 	<string name="subtitle_margin">次行距離</string>
 	<string name="heading_spacing">標題段距</string>
 	<string name="heading_spacing">標題段距</string>
+    <string name="title_spacing">間距</string>
     <string name="web_file">網頁文件</string>
     <string name="web_file">網頁文件</string>
     <string name="show_book_count">顯示分組書籍數量</string>
     <string name="show_book_count">顯示分組書籍數量</string>
     <string name="dynamic_colors_summary">使用基於桌布的主題顏色</string>
     <string name="dynamic_colors_summary">使用基於桌布的主題顏色</string>
@@ -1637,6 +1642,7 @@
     <string name="compose_engine">主題樣式</string>
     <string name="compose_engine">主題樣式</string>
     <string name="compose_related">Compose 相關</string>
     <string name="compose_related">Compose 相關</string>
     <string name="config_btn">配置底部按鈕</string>
     <string name="config_btn">配置底部按鈕</string>
+    <string name="more_actions_config">設定更多選單</string>
     <string name="confirm_skip_to_chapter">跳轉到此章節?</string>
     <string name="confirm_skip_to_chapter">跳轉到此章節?</string>
     <string name="container_background_color">容器背景色</string>
     <string name="container_background_color">容器背景色</string>
     <string name="container_opacity">容器不透明度</string>
     <string name="container_opacity">容器不透明度</string>
@@ -1945,7 +1951,7 @@
     <string name="read_change_all_s">否則僅應用於單章。</string>
     <string name="read_change_all_s">否則僅應用於單章。</string>
     <string name="read_chapter_index">已讀 %d 章</string>
     <string name="read_chapter_index">已讀 %d 章</string>
     <string name="read_chapter_total">共 %d 章</string>
     <string name="read_chapter_total">共 %d 章</string>
-    <string name="read_color">顏色樣式</string>
+    <string name="read_color">顏色</string>
     <string name="read_config_body_padding_desc">調整正文上下左右邊距</string>
     <string name="read_config_body_padding_desc">調整正文上下左右邊距</string>
     <string name="read_config_body_spacing">正文間距</string>
     <string name="read_config_body_spacing">正文間距</string>
     <string name="read_config_divider_line">分隔線</string>
     <string name="read_config_divider_line">分隔線</string>

+ 8 - 2
app/src/main/res/values-zh-rTW/strings.xml

@@ -210,7 +210,7 @@
     <string name="mouse_wheel_page">鼠标滚动翻页</string>
     <string name="mouse_wheel_page">鼠标滚动翻页</string>
     <string name="click_turn_page">點擊翻頁</string>
     <string name="click_turn_page">點擊翻頁</string>
     <string name="page_anim">翻頁動畫</string>
     <string name="page_anim">翻頁動畫</string>
-    <string name="book_page_anim">翻頁動畫(本書)</string>
+    <string name="book_page_anim">本書翻頁動畫</string>
     <string name="keep_light">螢幕超時</string>
     <string name="keep_light">螢幕超時</string>
     <string name="back">返回</string>
     <string name="back">返回</string>
     <string name="menu">選單</string>
     <string name="menu">選單</string>
@@ -847,6 +847,7 @@
     <string name="prev_page">上一頁</string>
     <string name="prev_page">上一頁</string>
     <string name="non_action">無操作</string>
     <string name="non_action">無操作</string>
     <string name="body_title">標題</string>
     <string name="body_title">標題</string>
+    <string name="title_position">標題位置</string>
     <string name="show_hide">顯示/隱藏</string>
     <string name="show_hide">顯示/隱藏</string>
     <string name="header_footer">頁首<![CDATA[&]]>頁尾</string>
     <string name="header_footer">頁首<![CDATA[&]]>頁尾</string>
     <string name="rule_subscription">規則訂閱</string>
     <string name="rule_subscription">規則訂閱</string>
@@ -998,6 +999,7 @@
     <string name="scope_content">作用於正文</string>
     <string name="scope_content">作用於正文</string>
     <string name="join_qq_channel">加入QQ頻道</string>
     <string name="join_qq_channel">加入QQ頻道</string>
     <string name="qq_channel_summary">點擊加入閱讀QQ頻道</string>
     <string name="qq_channel_summary">點擊加入閱讀QQ頻道</string>
+    <string name="menu_refresh">重新整理章節</string>
     <string name="menu_refresh_dur">重新整理目前章節</string>
     <string name="menu_refresh_dur">重新整理目前章節</string>
     <string name="menu_refresh_after">重新整理之後章節</string>
     <string name="menu_refresh_after">重新整理之後章節</string>
     <string name="menu_refresh_all">重新整理全部章節</string>
     <string name="menu_refresh_all">重新整理全部章節</string>
@@ -1120,6 +1122,7 @@
     <string name="show_last_update_time">顯示上次更新時間</string>
     <string name="show_last_update_time">顯示上次更新時間</string>
     <string name="refresh_list">刷新列表</string>
     <string name="refresh_list">刷新列表</string>
     <string name="tip_divider_color">分隔线颜色</string>
     <string name="tip_divider_color">分隔线颜色</string>
+    <string name="tip_divider_color_shared_desc">页眉与页脚共用</string>
     <string name="same_title_removed">移除重复标题</string>
     <string name="same_title_removed">移除重复标题</string>
     <string name="update_book_fail">更新失败</string>
     <string name="update_book_fail">更新失败</string>
     <string name="notification_permission_rationale">阅读需要发送通知来显示朗读控制和下载进度</string>
     <string name="notification_permission_rationale">阅读需要发送通知来显示朗读控制和下载进度</string>
@@ -1233,6 +1236,7 @@
     <string name="record_heap_dump_t">记录堆转储</string>
     <string name="record_heap_dump_t">记录堆转储</string>
     <string name="font_weight_text">字重</string>
     <string name="font_weight_text">字重</string>
     <string name="font_weight">字重</string>
     <string name="font_weight">字重</string>
+    <string name="font_weight_custom_description">自訂字重僅適用於可變字重的字體</string>
     <string name="keep_swipe_tip">继续滑动以加载下一章…</string>
     <string name="keep_swipe_tip">继续滑动以加载下一章…</string>
     <string name="enable_optimize_render">启用绘制优化</string>
     <string name="enable_optimize_render">启用绘制优化</string>
     <string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
     <string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
@@ -1311,6 +1315,7 @@
 	<string name="subtitle_scale">次行縮放</string>
 	<string name="subtitle_scale">次行縮放</string>
 	<string name="subtitle_margin">次行距離</string>
 	<string name="subtitle_margin">次行距離</string>
 	<string name="heading_spacing">標題段距</string>
 	<string name="heading_spacing">標題段距</string>
+    <string name="title_spacing">間距</string>
     <string name="bookshelf_total_count">共%1$d本</string>
     <string name="bookshelf_total_count">共%1$d本</string>
     <string name="bookshelf_selected_count">已選%1$d本</string>
     <string name="bookshelf_selected_count">已選%1$d本</string>
     <string name="bookshelf_empty_global_search">沒有書籍,嘗試全域搜尋</string>
     <string name="bookshelf_empty_global_search">沒有書籍,嘗試全域搜尋</string>
@@ -1731,6 +1736,7 @@
     <string name="welcome_title">歡迎!</string>
     <string name="welcome_title">歡迎!</string>
     <string name="first_read">請先閱讀服務條款和使用者協議。</string>
     <string name="first_read">請先閱讀服務條款和使用者協議。</string>
     <string name="config_btn">設定底部按鈕</string>
     <string name="config_btn">設定底部按鈕</string>
+    <string name="more_actions_config">設定更多選單</string>
     <string name="compact_grid">精簡網格</string>
     <string name="compact_grid">精簡網格</string>
     <string name="cover_grid">封面網格</string>
     <string name="cover_grid">封面網格</string>
     <string name="end_chapter">結束章節</string>
     <string name="end_chapter">結束章節</string>
@@ -1894,7 +1900,7 @@
     <string name="save_to_local">儲存到本機</string>
     <string name="save_to_local">儲存到本機</string>
     <string name="select_operation">選擇操作</string>
     <string name="select_operation">選擇操作</string>
     <string name="text_accent_color">強調色</string>
     <string name="text_accent_color">強調色</string>
-    <string name="read_color">色彩樣式</string>
+    <string name="read_color">顏色</string>
     <string name="reverse_volume_key_page">反轉音量鍵翻頁方向</string>
     <string name="reverse_volume_key_page">反轉音量鍵翻頁方向</string>
     <string name="palette_style">調色板樣式</string>
     <string name="palette_style">調色板樣式</string>
     <string name="material_version">色彩規格</string>
     <string name="material_version">色彩規格</string>

+ 13 - 3
app/src/main/res/values/strings.xml

@@ -1011,6 +1011,7 @@
     <string name="prev_page">Prior page</string>
     <string name="prev_page">Prior page</string>
     <string name="non_action">None</string>
     <string name="non_action">None</string>
     <string name="body_title">Title</string>
     <string name="body_title">Title</string>
+    <string name="title_position">Title Position</string>
     <string name="show_hide">Show/Hide</string>
     <string name="show_hide">Show/Hide</string>
     <string name="header_footer">Footer <![CDATA[&]]> Header</string>
     <string name="header_footer">Footer <![CDATA[&]]> Header</string>
     <string name="rule_subscription">Rule Subscription</string>
     <string name="rule_subscription">Rule Subscription</string>
@@ -1199,6 +1200,7 @@
     <string name="scope_content">Act on the content</string>
     <string name="scope_content">Act on the content</string>
     <string name="join_qq_channel">Join QQ channel</string>
     <string name="join_qq_channel">Join QQ channel</string>
     <string name="qq_channel_summary">Tap to join the Reading QQ channel</string>
     <string name="qq_channel_summary">Tap to join the Reading QQ channel</string>
+    <string name="menu_refresh">Refresh Chapter</string>
     <string name="menu_refresh_dur">Refresh the current chapter</string>
     <string name="menu_refresh_dur">Refresh the current chapter</string>
     <string name="menu_refresh_after">Refresh the chapter after</string>
     <string name="menu_refresh_after">Refresh the chapter after</string>
     <string name="menu_refresh_all">Refresh all chapters</string>
     <string name="menu_refresh_all">Refresh all chapters</string>
@@ -1345,6 +1347,7 @@
     <string name="show_last_update_time">Show Last Updated Time</string>
     <string name="show_last_update_time">Show Last Updated Time</string>
     <string name="refresh_list">Refresh list</string>
     <string name="refresh_list">Refresh list</string>
     <string name="tip_divider_color">Divider color</string>
     <string name="tip_divider_color">Divider color</string>
+    <string name="tip_divider_color_shared_desc">Shared by header and footer</string>
     <string name="same_title_removed">Remove Duplicate Title</string>
     <string name="same_title_removed">Remove Duplicate Title</string>
     <string name="update_book_fail">Update failed</string>
     <string name="update_book_fail">Update failed</string>
     <string name="notification_permission_rationale">Reading requires sending notifications to show reading control and download progress</string>
     <string name="notification_permission_rationale">Reading requires sending notifications to show reading control and download progress</string>
@@ -1477,6 +1480,7 @@
     <string name="record_heap_dump_t">Capture heap dump</string>
     <string name="record_heap_dump_t">Capture heap dump</string>
     <string name="font_weight_text">N/B/L</string>
     <string name="font_weight_text">N/B/L</string>
     <string name="font_weight">Font Weight</string>
     <string name="font_weight">Font Weight</string>
+    <string name="font_weight_custom_description">Custom weight only applies to variable fonts</string>
     <string name="keep_swipe_tip">Keep swiping to load the next chapter…</string>
     <string name="keep_swipe_tip">Keep swiping to load the next chapter…</string>
     <string name="enable_optimize_render">Enable Render Optimization</string>
     <string name="enable_optimize_render">Enable Render Optimization</string>
     <string name="ignore_battery_permission_rationale">Reading requires requesting background permission to keep the service running properly</string>
     <string name="ignore_battery_permission_rationale">Reading requires requesting background permission to keep the service running properly</string>
@@ -1643,6 +1647,7 @@
     <string name="welcome_book_folder_tip">Set where local books are stored. You can change it later in Settings.</string>
     <string name="welcome_book_folder_tip">Set where local books are stored. You can change it later in Settings.</string>
     <string name="welcome_book_folder_not_selected">No folder selected</string>
     <string name="welcome_book_folder_not_selected">No folder selected</string>
     <string name="config_btn">Configure Bottom Buttons</string>
     <string name="config_btn">Configure Bottom Buttons</string>
+    <string name="more_actions_config">Configure More Menu</string>
     <string name="compact_grid">Compact Grid</string>
     <string name="compact_grid">Compact Grid</string>
     <string name="cover_grid">Cover Grid</string>
     <string name="cover_grid">Cover Grid</string>
     <string name="end_chapter">End Chapter</string>
     <string name="end_chapter">End Chapter</string>
@@ -1819,7 +1824,7 @@
     <string name="save_to_local">Save to local</string>
     <string name="save_to_local">Save to local</string>
     <string name="select_operation">Select an operation</string>
     <string name="select_operation">Select an operation</string>
     <string name="text_accent_color">Accent Color</string>
     <string name="text_accent_color">Accent Color</string>
-    <string name="read_color">Color Style</string>
+    <string name="read_color">Color</string>
     <string name="reverse_volume_key_page">Reverse Volume-Key Page Turn Direction</string>
     <string name="reverse_volume_key_page">Reverse Volume-Key Page Turn Direction</string>
     <string name="palette_style">Palette Style</string>
     <string name="palette_style">Palette Style</string>
     <string name="material_version">Color Specification</string>
     <string name="material_version">Color Specification</string>
@@ -1858,9 +1863,7 @@
     <string name="filter_show_not_in_shelf_only">Show Only Books Not in Shelf</string>
     <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="follow_read_background">Follow background</string>
     <string name="heading_spacing">Heading spacing</string>
     <string name="heading_spacing">Heading spacing</string>
-    <string name="title_segment">Segment</string>
     <string name="title_spacing">Spacing</string>
     <string name="title_spacing">Spacing</string>
-    <string name="title_dedup">Dedup</string>
     <string name="layout_setting">Layout settings</string>
     <string name="layout_setting">Layout settings</string>
     <string name="rule_segment">Rule</string>
     <string name="rule_segment">Rule</string>
     <string name="split_title">Split title</string>
     <string name="split_title">Split title</string>
@@ -2015,6 +2018,13 @@
     <string name="ai_rewrite_preset_dialogue_name">Natural dialogue</string>
     <string name="ai_rewrite_preset_dialogue_name">Natural dialogue</string>
     <string name="ai_rewrite_preset_dialogue_instruction">Rewrite the selected text into more natural spoken narration or dialogue while preserving character intent, relationships, and plot facts.</string>
     <string name="ai_rewrite_preset_dialogue_instruction">Rewrite the selected text into more natural spoken narration or dialogue while preserving character intent, relationships, and plot facts.</string>
     <string name="content_processes">Content edits</string>
     <string name="content_processes">Content edits</string>
+    <string name="text_processing">Text processing</string>
+    <string name="replace_filter_all">All</string>
+    <string name="replace_filter">Filter replacement rules</string>
+    <string name="replace_filter_enabled">Enabled</string>
+    <string name="replace_filter_disabled">Disabled</string>
+    <string name="replace_filter_effective">Effective now</string>
+    <string name="replace_filter_empty">No matching replacement rules</string>
     <string name="content_process_empty">No content edits for this chapter.</string>
     <string name="content_process_empty">No content edits for this chapter.</string>
     <string name="content_process_delete_confirm">Delete this content edit?\n\n%1$s</string>
     <string name="content_process_delete_confirm">Delete this content edit?\n\n%1$s</string>
     <string name="content_process_delete_confirm_simple">Delete this content edit?</string>
     <string name="content_process_delete_confirm_simple">Delete this content edit?</string>