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

refactor(Track E·E3/R1.2): 阅读设置弹层去可变全局直读

弹层此前在组合期直读 `ReadBookConfig`——上游变化不触发重组,seed 进
`remember` 也只在首次组合读一次,是「改完关掉再打开还是旧值」的另一半根因
(R1.1 收敛的是 VM 侧重建,这里是 UI 侧读取)。

HeaderFooterPage(69 处 → 只剩常量):
- 删掉 19 个镜像本地 state,开关/下拉直读 `config.x`,写入仍走 ConfigUpdate;
- 8 个 padding 滑块与字号滑块保留本地暂存(连续 Float vs 持久化 Int),
  但改 `remember(config.x)` 带 key,上游变化会重新 seed;
- 颜色回退(tip*Color == 0 时用正文色)改读快照,表达式与原逻辑等价
  (`ReadBookConfig.textColor` 与 `sheetConfig.textColorDay` 同为
  `durConfig.curTextColor()`)。

SystemMenuPage(4 处):菜单配色回退值改读 `styleConfig`,为此给
ReadBookStyleConfig 补 menuBgColor/menuAccentColor 的日夜四值。

CustomTipTarget 的 `tipValue`/`customTemplate` 改成吃快照的
`tipValueOf(config)`/`customTemplateOf(config)`;PageView 作为渲染层
仍需直读全局(属 E5/D2 范围),故在其内部新增等价私有扩展。

新增 SheetGlobalConfigReadTest 护栏:扫描 sheet/ 目录,除常量白名单
(tipNone/tipCustom/tipNames/tipValues/get*Modes)外任何 `ReadBookConfig.`
直读即报红。已用注入直读验证其确实变红。

验证:compileAppDebugKotlin + 411 单测全绿。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5151561 3 месяцев назад
Родитель
Сommit
ca66ec4bfa

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

@@ -92,6 +92,11 @@ data class ReadBookStyleConfig(
     val pageAnimEInk: Int = 4,
     val pageAnimEInk: Int = 4,
     // Layout
     // Layout
     val shareLayout: Boolean = false,
     val shareLayout: Boolean = false,
+    // 排版自带的阅读菜单配色,作为 DataStore 里 readMenu*Color 为 0(未自定义)时的回退
+    val menuBgColorDay: Int = 0,
+    val menuBgColorNight: Int = 0,
+    val menuAccentColorDay: Int = 0,
+    val menuAccentColorNight: Int = 0,
     // Config list for style selector
     // Config list for style selector
     val configCount: Int = 1,
     val configCount: Int = 1,
     val styleItems: ImmutableList<ReadStyleItem> = persistentListOf(),
     val styleItems: ImmutableList<ReadStyleItem> = persistentListOf(),
@@ -158,7 +163,25 @@ data class ReadSheetConfigUiState(
     val headerFontSize: Int = 12,
     val headerFontSize: Int = 12,
     val footerFontSize: Int = 12,
     val footerFontSize: Int = 12,
     val applyHeaderStyle: Boolean = true,
     val applyHeaderStyle: Boolean = true,
+    /** -1 表示"跟随主题分割线",0 表示"跟随正文颜色"。 */
     val tipDividerColor: Int = 0,
     val tipDividerColor: Int = 0,
+    val headerMode: Int = 0,
+    val footerMode: Int = 0,
+    val showHeaderLine: Boolean = false,
+    val showFooterLine: Boolean = false,
+    val tipHeaderLeft: Int = 0,
+    val tipHeaderMiddle: Int = 0,
+    val tipHeaderRight: Int = 0,
+    val tipFooterLeft: Int = 0,
+    val tipFooterMiddle: Int = 0,
+    val tipFooterRight: Int = 0,
+    val customTipHeaderLeft: String = "",
+    val customTipHeaderMiddle: String = "",
+    val customTipHeaderRight: String = "",
+    val customTipFooterLeft: String = "",
+    val customTipFooterMiddle: String = "",
+    val customTipFooterRight: String = "",
+    /** 0 表示"跟随正文颜色",展示时回退到 [textColorDay] / [textColorNight]。 */
     val tipHeaderColor: Int = 0,
     val tipHeaderColor: Int = 0,
     val tipHeaderColorNight: Int = 0,
     val tipHeaderColorNight: Int = 0,
     val tipFooterColor: Int = 0,
     val tipFooterColor: Int = 0,

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

@@ -2433,6 +2433,10 @@ class ReadBookViewModel(
             pageAnim = actualConfig.getPageAnim(),
             pageAnim = actualConfig.getPageAnim(),
             pageAnimEInk = actualConfig.getPageAnimEInk(),
             pageAnimEInk = actualConfig.getPageAnimEInk(),
             shareLayout = config.shareLayout,
             shareLayout = config.shareLayout,
+            menuBgColorDay = dur.menuBgColor(isNight = false),
+            menuBgColorNight = dur.menuBgColor(isNight = true),
+            menuAccentColorDay = dur.menuAccentColor(isNight = false),
+            menuAccentColorNight = dur.menuAccentColor(isNight = true),
             configCount = styleState.items.size,
             configCount = styleState.items.size,
             styleItems = styleState.items.toImmutableList(),
             styleItems = styleState.items.toImmutableList(),
         )
         )
@@ -2494,6 +2498,22 @@ class ReadBookViewModel(
         footerFontSize = ReadBookConfig.footerFontSize,
         footerFontSize = ReadBookConfig.footerFontSize,
         applyHeaderStyle = ReadBookConfig.applyHeaderStyle,
         applyHeaderStyle = ReadBookConfig.applyHeaderStyle,
         tipDividerColor = ReadBookConfig.tipDividerColor,
         tipDividerColor = ReadBookConfig.tipDividerColor,
+        headerMode = ReadBookConfig.headerMode,
+        footerMode = ReadBookConfig.footerMode,
+        showHeaderLine = ReadBookConfig.showHeaderLine,
+        showFooterLine = ReadBookConfig.showFooterLine,
+        tipHeaderLeft = ReadBookConfig.tipHeaderLeft,
+        tipHeaderMiddle = ReadBookConfig.tipHeaderMiddle,
+        tipHeaderRight = ReadBookConfig.tipHeaderRight,
+        tipFooterLeft = ReadBookConfig.tipFooterLeft,
+        tipFooterMiddle = ReadBookConfig.tipFooterMiddle,
+        tipFooterRight = ReadBookConfig.tipFooterRight,
+        customTipHeaderLeft = ReadBookConfig.customTipHeaderLeft,
+        customTipHeaderMiddle = ReadBookConfig.customTipHeaderMiddle,
+        customTipHeaderRight = ReadBookConfig.customTipHeaderRight,
+        customTipFooterLeft = ReadBookConfig.customTipFooterLeft,
+        customTipFooterMiddle = ReadBookConfig.customTipFooterMiddle,
+        customTipFooterRight = ReadBookConfig.customTipFooterRight,
         tipHeaderColor = ReadBookConfig.tipHeaderColor,
         tipHeaderColor = ReadBookConfig.tipHeaderColor,
         tipHeaderColorNight = ReadBookConfig.tipHeaderColorNight,
         tipHeaderColorNight = ReadBookConfig.tipHeaderColorNight,
         tipFooterColor = ReadBookConfig.tipFooterColor,
         tipFooterColor = ReadBookConfig.tipFooterColor,

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

@@ -419,7 +419,7 @@ class PageView(
         val customTypeface = tipTypeface
         val customTypeface = tipTypeface
         val customTextSize = tipTextSize
         val customTextSize = tipTextSize
         for (target in CUSTOM_TIP_TARGETS) {
         for (target in CUSTOM_TIP_TARGETS) {
-            if (target.tipValue != ReadBookConfig.tipCustom) continue
+            if (target.currentTipValue() != ReadBookConfig.tipCustom) continue
             val view = customTipViewFor(target)
             val view = customTipViewFor(target)
             view.tag = ReadBookConfig.tipCustom
             view.tag = ReadBookConfig.tipCustom
             view.typeface = customTypeface
             view.typeface = customTypeface
@@ -535,8 +535,8 @@ class PageView(
     fun upCustomTip(textPage: TextPage?) {
     fun upCustomTip(textPage: TextPage?) {
         val page = textPage ?: return
         val page = textPage ?: return
         for (target in CUSTOM_TIP_TARGETS) {
         for (target in CUSTOM_TIP_TARGETS) {
-            if (target.tipValue != ReadBookConfig.tipCustom) continue
-            val template = target.customTemplate
+            if (target.currentTipValue() != ReadBookConfig.tipCustom) continue
+            val template = target.currentCustomTemplate()
             val view = customTipViewFor(target)
             val view = customTipViewFor(target)
             if (template.isEmpty()) {
             if (template.isEmpty()) {
                 view.setTextIfNotEqual("")
                 view.setTextIfNotEqual("")
@@ -841,3 +841,28 @@ class PageView(
         private val CUSTOM_TIP_PLACEHOLDERS: Array<CustomTipPlaceholder> = CustomTipPlaceholder.values()
         private val CUSTOM_TIP_PLACEHOLDERS: Array<CustomTipPlaceholder> = CustomTipPlaceholder.values()
     }
     }
 }
 }
+
+/**
+ * 渲染层按位置取当前 tip 类型 / 自定义模板。
+ *
+ * 直读可变全局 [ReadBookConfig]——与本文件其余渲染读取一致,属排版引擎去全局化
+ * (Track E·E5 / Track D2)的范围,不在设置面 UDF 改造内。设置面读的是
+ * `ReadSheetConfigUiState` 快照(`CustomTipTarget.tipValueOf` / `customTemplateOf`)。
+ */
+private fun CustomTipTarget.currentTipValue(): Int = when (this) {
+    CustomTipTarget.HEADER_LEFT -> ReadBookConfig.tipHeaderLeft
+    CustomTipTarget.HEADER_MIDDLE -> ReadBookConfig.tipHeaderMiddle
+    CustomTipTarget.HEADER_RIGHT -> ReadBookConfig.tipHeaderRight
+    CustomTipTarget.FOOTER_LEFT -> ReadBookConfig.tipFooterLeft
+    CustomTipTarget.FOOTER_MIDDLE -> ReadBookConfig.tipFooterMiddle
+    CustomTipTarget.FOOTER_RIGHT -> ReadBookConfig.tipFooterRight
+}
+
+private fun CustomTipTarget.currentCustomTemplate(): String = when (this) {
+    CustomTipTarget.HEADER_LEFT -> ReadBookConfig.customTipHeaderLeft
+    CustomTipTarget.HEADER_MIDDLE -> ReadBookConfig.customTipHeaderMiddle
+    CustomTipTarget.HEADER_RIGHT -> ReadBookConfig.customTipHeaderRight
+    CustomTipTarget.FOOTER_LEFT -> ReadBookConfig.customTipFooterLeft
+    CustomTipTarget.FOOTER_MIDDLE -> ReadBookConfig.customTipFooterMiddle
+    CustomTipTarget.FOOTER_RIGHT -> ReadBookConfig.customTipFooterRight
+}

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

@@ -1,17 +1,17 @@
 package io.legado.app.ui.book.read.sheet
 package io.legado.app.ui.book.read.sheet
 
 
-import io.legado.app.help.config.ReadBookConfig
 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
 
 
 /**
 /**
  * 标识页眉/页脚的 6 个位置之一,用于自定义模板编辑。
  * 标识页眉/页脚的 6 个位置之一,用于自定义模板编辑。
  *
  *
  * 每个目标集中管理:
  * 每个目标集中管理:
- *  - [tipValue]:该位置在 [ReadBookConfig] 中保存的 tip 类型常量
- *  - [customTemplate]:自定义模板字符串的只读 getter(读取 [ReadBookConfig])
+ *  - [tipValueOf]:该位置当前选中的 tip 类型(从 UiState 快照读,不读可变全局)
+ *  - [customTemplateOf]:该位置的自定义模板字符串(同上)
  *  - [configUpdate]:持久化时使用的 [ConfigUpdate] 工厂
  *  - [configUpdate]:持久化时使用的 [ConfigUpdate] 工厂
- *  - [applyTemplate]:把模板保存到 [ReadBookConfig] 并派发 [ReadBookIntent.UpdateConfig]
+ *  - [applyTemplate]:派发 [ReadBookIntent.UpdateConfig] 走 gateway 持久化
  *
  *
  * 这样的设计让 6 个位置的逻辑只在一处声明,避免散落各处的 if 链。
  * 这样的设计让 6 个位置的逻辑只在一处声明,避免散落各处的 if 链。
  */
  */
@@ -37,24 +37,22 @@ internal enum class CustomTipTarget {
         FOOTER_RIGHT -> ConfigUpdate.CustomTipFooterRight(template)
         FOOTER_RIGHT -> ConfigUpdate.CustomTipFooterRight(template)
     }
     }
 
 
-    /** 该位置当前选中的 tip 类型(如 [ReadBookConfig.tipCustom] / [ReadBookConfig.tipBookName] 等)。 */
-    val tipValue: Int
-        get() = when (this) {
-            HEADER_LEFT -> ReadBookConfig.tipHeaderLeft
-            HEADER_MIDDLE -> ReadBookConfig.tipHeaderMiddle
-            HEADER_RIGHT -> ReadBookConfig.tipHeaderRight
-            FOOTER_LEFT -> ReadBookConfig.tipFooterLeft
-            FOOTER_MIDDLE -> ReadBookConfig.tipFooterMiddle
-            FOOTER_RIGHT -> ReadBookConfig.tipFooterRight
-        }
+    /** 该位置当前选中的 tip 类型(tipNone / tipCustom / tipBookName 等)。 */
+    fun tipValueOf(config: ReadSheetConfigUiState): Int = when (this) {
+        HEADER_LEFT -> config.tipHeaderLeft
+        HEADER_MIDDLE -> config.tipHeaderMiddle
+        HEADER_RIGHT -> config.tipHeaderRight
+        FOOTER_LEFT -> config.tipFooterLeft
+        FOOTER_MIDDLE -> config.tipFooterMiddle
+        FOOTER_RIGHT -> config.tipFooterRight
+    }
 
 
-    val customTemplate: String
-        get() = when (this) {
-            HEADER_LEFT -> ReadBookConfig.customTipHeaderLeft
-            HEADER_MIDDLE -> ReadBookConfig.customTipHeaderMiddle
-            HEADER_RIGHT -> ReadBookConfig.customTipHeaderRight
-            FOOTER_LEFT -> ReadBookConfig.customTipFooterLeft
-            FOOTER_MIDDLE -> ReadBookConfig.customTipFooterMiddle
-            FOOTER_RIGHT -> ReadBookConfig.customTipFooterRight
-        }
+    fun customTemplateOf(config: ReadSheetConfigUiState): String = when (this) {
+        HEADER_LEFT -> config.customTipHeaderLeft
+        HEADER_MIDDLE -> config.customTipHeaderMiddle
+        HEADER_RIGHT -> config.customTipHeaderRight
+        FOOTER_LEFT -> config.customTipFooterLeft
+        FOOTER_MIDDLE -> config.customTipFooterMiddle
+        FOOTER_RIGHT -> config.customTipFooterRight
+    }
 }
 }

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

@@ -113,6 +113,7 @@ fun ReadStyleContent(
 
 
                     1 -> SystemMenuPage(
                     1 -> SystemMenuPage(
                         preferences = preferences,
                         preferences = preferences,
+                        styleConfig = styleConfig,
                         customIcons = readMenuCustomIcons,
                         customIcons = readMenuCustomIcons,
                         bottomBarButtons = bottomBarButtons,
                         bottomBarButtons = bottomBarButtons,
                         onIntent = onIntent,
                         onIntent = onIntent,

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

@@ -62,6 +62,7 @@ import io.legado.app.help.config.ReadBookConfig
 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.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
@@ -97,6 +98,7 @@ private const val COLOR_MENU_TEXT_NIGHT = 16
 @Composable
 @Composable
 internal fun SystemMenuPage(
 internal fun SystemMenuPage(
     preferences: ReadPreferences,
     preferences: ReadPreferences,
+    styleConfig: ReadBookStyleConfig,
     customIcons: Map<String, String>,
     customIcons: Map<String, String>,
     bottomBarButtons: List<ReadBookButtonConfigItem>,
     bottomBarButtons: List<ReadBookButtonConfigItem>,
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
@@ -176,6 +178,7 @@ internal fun SystemMenuPage(
                 when (page) {
                 when (page) {
                     0 -> GlobalMenuTab(
                     0 -> GlobalMenuTab(
                         preferences = preferences,
                         preferences = preferences,
+                        styleConfig = styleConfig,
                         onIntent = onIntent,
                         onIntent = onIntent,
                         onShowColorPicker = { id, initial ->
                         onShowColorPicker = { id, initial ->
                             colorPickerId = id
                             colorPickerId = id
@@ -240,6 +243,7 @@ internal fun SystemMenuPage(
 @Composable
 @Composable
 private fun GlobalMenuTab(
 private fun GlobalMenuTab(
     preferences: ReadPreferences,
     preferences: ReadPreferences,
+    styleConfig: ReadBookStyleConfig,
     onIntent: (ReadBookIntent) -> Unit,
     onIntent: (ReadBookIntent) -> Unit,
     onShowColorPicker: (Int, Int) -> Unit,
     onShowColorPicker: (Int, Int) -> Unit,
 ) {
 ) {
@@ -250,16 +254,16 @@ private fun GlobalMenuTab(
     val colorMode = preferences.readMenuColorMode.coerceIn(0, 1)
     val colorMode = preferences.readMenuColorMode.coerceIn(0, 1)
     val dayMenuBgColor = preferences.readMenuBgColor
     val dayMenuBgColor = preferences.readMenuBgColor
         .takeIf { it != 0 }
         .takeIf { it != 0 }
-        ?: ReadBookConfig.durConfig.menuBgColor(isNight = false)
+        ?: styleConfig.menuBgColorDay
     val nightMenuBgColor = preferences.readMenuBgColorNight
     val nightMenuBgColor = preferences.readMenuBgColorNight
         .takeIf { it != 0 }
         .takeIf { it != 0 }
-        ?: ReadBookConfig.durConfig.menuBgColor(isNight = true)
+        ?: styleConfig.menuBgColorNight
     val dayMenuAccentColor = preferences.readMenuAccentColor
     val dayMenuAccentColor = preferences.readMenuAccentColor
         .takeIf { it != 0 }
         .takeIf { it != 0 }
-        ?: ReadBookConfig.durConfig.menuAccentColor(isNight = false)
+        ?: styleConfig.menuAccentColorDay
     val nightMenuAccentColor = preferences.readMenuAccentColorNight
     val nightMenuAccentColor = preferences.readMenuAccentColorNight
         .takeIf { it != 0 }
         .takeIf { it != 0 }
-        ?: ReadBookConfig.durConfig.menuAccentColor(isNight = true)
+        ?: styleConfig.menuAccentColorNight
     val dayMenuContainerColor = preferences.readMenuContainerColor
     val dayMenuContainerColor = preferences.readMenuContainerColor
         .takeIf { it != 0 }
         .takeIf { it != 0 }
         ?: dayMenuBgColor
         ?: dayMenuBgColor

+ 68 - 0
app/src/test/java/io/legado/app/ui/book/read/sheet/SheetGlobalConfigReadTest.kt

@@ -0,0 +1,68 @@
+package io.legado.app.ui.book.read.sheet
+
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import java.io.File
+
+/**
+ * Track E · E3 —— 阅读设置弹层不得读可变全局排版配置。
+ *
+ * 弹层的显示值必须来自 `ReadBookUiState` 的快照(`sheetConfig` / `styleConfig`),
+ * 否则会出现「改完关掉再打开还是旧值」——组合期直读 `ReadBookConfig` 的可变字段时,
+ * 上游变化不会触发重组;即使 seed 进 `remember` 也只在首次组合读一次。
+ *
+ * 例外是编译期常量与静态枚举元数据(tip 类型码、下拉项名称/取值),它们不随配置变化,
+ * 见 [ALLOWED_MEMBERS]。
+ */
+class SheetGlobalConfigReadTest {
+
+    @Test
+    fun `设置弹层不直读可变的 ReadBookConfig 字段`() {
+        val offenders = sheetSources().flatMap { file ->
+            GLOBAL_ACCESS.findAll(file.readText())
+                .map { it.groupValues[1] }
+                .filterNot { it in ALLOWED_MEMBERS }
+                .map { "${file.name} → ReadBookConfig.$it" }
+        }.distinct().sorted()
+
+        assertEquals(
+            "以下弹层代码直读了可变全局配置,请改从 state.sheetConfig / state.styleConfig 取值" +
+                "(若确为常量或静态元数据,加进 ALLOWED_MEMBERS 并说明理由):\n" +
+                offenders.joinToString("\n") { "  - $it" },
+            emptyList<String>(),
+            offenders,
+        )
+    }
+
+    private fun sheetSources(): List<File> =
+        sheetDirectory().listFiles { f: File -> f.isFile && f.extension == "kt" }
+            .orEmpty()
+            .sortedBy(File::getName)
+
+    private fun sheetDirectory(): File {
+        val relativePath = "io/legado/app/ui/book/read/sheet"
+        var directory: File? = File("").absoluteFile
+        while (directory != null) {
+            for (prefix in listOf("src/main/java", "app/src/main/java")) {
+                val candidate = File(directory, "$prefix/$relativePath")
+                if (candidate.isDirectory) return candidate
+            }
+            directory = directory.parentFile
+        }
+        error("从 ${File("").absolutePath} 向上找不到 $relativePath")
+    }
+
+    private companion object {
+        val GLOBAL_ACCESS = Regex("""\bReadBookConfig\.([A-Za-z][A-Za-z0-9_]*)""")
+
+        /** 编译期常量与静态元数据——不随配置变化,读它们不会产生陈旧显示。 */
+        val ALLOWED_MEMBERS = setOf(
+            "tipNone",
+            "tipCustom",
+            "tipNames",
+            "tipValues",
+            "getHeaderModes",
+            "getFooterModes",
+        )
+    }
+}