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

refactor(R4.4): 排版配置的写面收进 gateway 实现,ReadBookConfig 降为只读投影

R4「配置底座」的所有权反转第一刀。先量后动,量出来的结论跟路线图的假设不同:
**`ReadBookStyleConfigRepository` 是全 main 源码里唯一写 `ReadBookConfig` 的地方**,
零个其他写点——R1/R2/R4.3 早把写路径收拢了。所以第一刀不该是搬状态,而是把写面
收进 gateway 实现、让 `ReadBookConfig` 变成只读投影。

75 个 setter 逐个核过,**全是一行、零隐藏逻辑**:70 个 `config.x = value`,另外 3 个
是模式感知方法(`setCurPageAnim`/`setUnderlineColor`/`setCurShadColor`),与 repository
里本来就直写的 `durConfig.setCurTextColor(value)` 同型。于是:

- repository 的 5 个 dispatch 函数开头取一次 `val config = ReadBookConfig.config`,
  直接写 `Config` 字段。原先走 `durConfig` 的条目(BgType/DottedBase/StatusIconDark 等)
  **保持不动**——`config` 在 shareLayout 打开时指向 shareConfig,两者不能混。
- `ReadBookConfig` 74 个 `var` → `val`,只剩 `durConfig` 一个写入口(整份配置替换:
  应用预设 / 导入)。1405 → 1164 行。
- 顺带带走 6 个从来没人写过的 setter:footerFont、footerFontSize、dottedBase、
  dottedRatio、underlineColor、shadowColor。

**一次编译通过**,等于编译器在全仓范围内复核了「repository 是唯一写点」这个前提,
比 grep 的结论硬。

本项目已有 Gradle 级架构护栏 `verifyConfigArchitecture`(build.gradle.kts:187,所有
`assemble*`/`compile*` 都 dependsOn 它),其中一条就是「ReadBookConfig 写入必须经过
ReadStyleGateway」。注入回归时是**它**先红的,单测都没轮上跑——为此写的同名单测是
重复的,已删。

不变式分两半把守,注释里写清了归属:**调用点**归那条 Gradle 规则(编译期拦,还管
`durConfig.setXxx(` 调用);**声明点**归 `ReadBookConfigDependencyDirectionTest` 新增的
「ReadBookConfig 是只读投影」——文件内部写的是 `config.x = value`,绕不过那条正则,
只能在这里断言。加回一个 setter 会让它红,反证过。全量单测 448 全绿。

状态本体(`configList`/`shareConfig`)仍在 `ReadBookConfig`;读面消费者一个没动
(ReadBookViewModel 114 处、ChapterProvider 75 处、repository 自身 45 处)。下一刀应是
给 `ReadStyleGateway` 加一等读面,再把 `configList` 搬进 repository。

真机验过(SM-S9310):改排版设置后杀进程重开保留、应用预设、增删排版方案、导入导出、
共享排版开关,均正常。

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

+ 76 - 71
app/src/main/java/io/legado/app/data/repository/ReadBookStyleConfigRepository.kt

@@ -168,92 +168,96 @@ class ReadBookStyleConfigRepository(
     }
 
     private fun updateInt(key: ReadStyleIntKey, value: Int) {
+        val config = ReadBookConfig.config
         when (key) {
-            ReadStyleIntKey.TextSize -> ReadBookConfig.textSize = value
-            ReadStyleIntKey.LineSpacing -> ReadBookConfig.lineSpacingExtra = value
-            ReadStyleIntKey.ParagraphSpacing -> ReadBookConfig.paragraphSpacing = value
-            ReadStyleIntKey.TextBold -> ReadBookConfig.textBold = value
-            ReadStyleIntKey.TitleMode -> ReadBookConfig.titleMode = value
-            ReadStyleIntKey.TitleBold -> ReadBookConfig.titleBold = value
-            ReadStyleIntKey.TitleLineSpacingExtra -> ReadBookConfig.titleLineSpacingExtra = value
-            ReadStyleIntKey.TitleLineSpacingSub -> ReadBookConfig.titleLineSpacingSub = value
-            ReadStyleIntKey.TitleSize -> ReadBookConfig.titleSize = value
-            ReadStyleIntKey.TitleTopSpacing -> ReadBookConfig.titleTopSpacing = value
-            ReadStyleIntKey.TitleBottomSpacing -> ReadBookConfig.titleBottomSpacing = value
-            ReadStyleIntKey.TitleSegType -> ReadBookConfig.titleSegType = value
-            ReadStyleIntKey.TitleSegDistance -> ReadBookConfig.titleSegDistance = value
-            ReadStyleIntKey.HeaderMode -> ReadBookConfig.headerMode = value
-            ReadStyleIntKey.FooterMode -> ReadBookConfig.footerMode = value
-            ReadStyleIntKey.TipHeaderLeft -> ReadBookConfig.tipHeaderLeft = value
-            ReadStyleIntKey.TipHeaderMiddle -> ReadBookConfig.tipHeaderMiddle = value
-            ReadStyleIntKey.TipHeaderRight -> ReadBookConfig.tipHeaderRight = value
-            ReadStyleIntKey.TipFooterLeft -> ReadBookConfig.tipFooterLeft = value
-            ReadStyleIntKey.TipFooterMiddle -> ReadBookConfig.tipFooterMiddle = value
-            ReadStyleIntKey.TipFooterRight -> ReadBookConfig.tipFooterRight = value
-            ReadStyleIntKey.HeaderFontSize -> ReadBookConfig.headerFontSize = value
-            ReadStyleIntKey.FooterFontSize -> ReadBookConfig.footerFontSize = value
-            ReadStyleIntKey.PageAnim -> ReadBookConfig.pageAnim = value
-            ReadStyleIntKey.UnderlineHeight -> ReadBookConfig.underlineHeight = value
-            ReadStyleIntKey.UnderlinePadding -> ReadBookConfig.underlinePadding = value
-            ReadStyleIntKey.PaddingTop -> ReadBookConfig.paddingTop = value
-            ReadStyleIntKey.PaddingBottom -> ReadBookConfig.paddingBottom = value
-            ReadStyleIntKey.PaddingLeft -> ReadBookConfig.paddingLeft = value
-            ReadStyleIntKey.PaddingRight -> ReadBookConfig.paddingRight = value
-            ReadStyleIntKey.HeaderPaddingTop -> ReadBookConfig.headerPaddingTop = value
-            ReadStyleIntKey.HeaderPaddingBottom -> ReadBookConfig.headerPaddingBottom = value
-            ReadStyleIntKey.HeaderPaddingLeft -> ReadBookConfig.headerPaddingLeft = value
-            ReadStyleIntKey.HeaderPaddingRight -> ReadBookConfig.headerPaddingRight = value
-            ReadStyleIntKey.FooterPaddingTop -> ReadBookConfig.footerPaddingTop = value
-            ReadStyleIntKey.FooterPaddingBottom -> ReadBookConfig.footerPaddingBottom = value
-            ReadStyleIntKey.FooterPaddingLeft -> ReadBookConfig.footerPaddingLeft = value
-            ReadStyleIntKey.FooterPaddingRight -> ReadBookConfig.footerPaddingRight = value
+            ReadStyleIntKey.TextSize -> config.textSize = value
+            ReadStyleIntKey.LineSpacing -> config.lineSpacingExtra = value
+            ReadStyleIntKey.ParagraphSpacing -> config.paragraphSpacing = value
+            ReadStyleIntKey.TextBold -> config.textBold = value
+            ReadStyleIntKey.TitleMode -> config.titleMode = value
+            ReadStyleIntKey.TitleBold -> config.titleBold = value
+            ReadStyleIntKey.TitleLineSpacingExtra -> config.titleLineSpacingExtra = value
+            ReadStyleIntKey.TitleLineSpacingSub -> config.titleLineSpacingSub = value
+            ReadStyleIntKey.TitleSize -> config.titleSize = value
+            ReadStyleIntKey.TitleTopSpacing -> config.titleTopSpacing = value
+            ReadStyleIntKey.TitleBottomSpacing -> config.titleBottomSpacing = value
+            ReadStyleIntKey.TitleSegType -> config.titleSegType = value
+            ReadStyleIntKey.TitleSegDistance -> config.titleSegDistance = value
+            ReadStyleIntKey.HeaderMode -> config.headerMode = value
+            ReadStyleIntKey.FooterMode -> config.footerMode = value
+            ReadStyleIntKey.TipHeaderLeft -> config.tipHeaderLeft = value
+            ReadStyleIntKey.TipHeaderMiddle -> config.tipHeaderMiddle = value
+            ReadStyleIntKey.TipHeaderRight -> config.tipHeaderRight = value
+            ReadStyleIntKey.TipFooterLeft -> config.tipFooterLeft = value
+            ReadStyleIntKey.TipFooterMiddle -> config.tipFooterMiddle = value
+            ReadStyleIntKey.TipFooterRight -> config.tipFooterRight = value
+            ReadStyleIntKey.HeaderFontSize -> config.headerFontSize = value
+            ReadStyleIntKey.FooterFontSize -> config.footerFontSize = value
+            ReadStyleIntKey.PageAnim -> config.setCurPageAnim(value)
+            ReadStyleIntKey.UnderlineHeight -> config.underlineHeight = value
+            ReadStyleIntKey.UnderlinePadding -> config.underlinePadding = value
+            ReadStyleIntKey.PaddingTop -> config.paddingTop = value
+            ReadStyleIntKey.PaddingBottom -> config.paddingBottom = value
+            ReadStyleIntKey.PaddingLeft -> config.paddingLeft = value
+            ReadStyleIntKey.PaddingRight -> config.paddingRight = value
+            ReadStyleIntKey.HeaderPaddingTop -> config.headerPaddingTop = value
+            ReadStyleIntKey.HeaderPaddingBottom -> config.headerPaddingBottom = value
+            ReadStyleIntKey.HeaderPaddingLeft -> config.headerPaddingLeft = value
+            ReadStyleIntKey.HeaderPaddingRight -> config.headerPaddingRight = value
+            ReadStyleIntKey.FooterPaddingTop -> config.footerPaddingTop = value
+            ReadStyleIntKey.FooterPaddingBottom -> config.footerPaddingBottom = value
+            ReadStyleIntKey.FooterPaddingLeft -> config.footerPaddingLeft = value
+            ReadStyleIntKey.FooterPaddingRight -> config.footerPaddingRight = value
             ReadStyleIntKey.BgType -> ReadBookConfig.durConfig.bgType = value
             ReadStyleIntKey.BgTypeNight -> ReadBookConfig.durConfig.bgTypeNight = value
             ReadStyleIntKey.BgTypeEInk -> ReadBookConfig.durConfig.bgTypeEInk = value
-            ReadStyleIntKey.BgAlpha -> ReadBookConfig.bgAlpha = value
+            ReadStyleIntKey.BgAlpha -> config.bgAlpha = value
         }
     }
 
     private fun updateFloat(key: ReadStyleFloatKey, value: Float) {
+        val config = ReadBookConfig.config
         when (key) {
-            ReadStyleFloatKey.LetterSpacing -> ReadBookConfig.letterSpacing = value
-            ReadStyleFloatKey.TitleSegScaling -> ReadBookConfig.titleSegScaling = value
-            ReadStyleFloatKey.ShadowRadius -> ReadBookConfig.shadowRadius = value
-            ReadStyleFloatKey.ShadowDx -> ReadBookConfig.shadowDx = value
-            ReadStyleFloatKey.ShadowDy -> ReadBookConfig.shadowDy = value
+            ReadStyleFloatKey.LetterSpacing -> config.letterSpacing = value
+            ReadStyleFloatKey.TitleSegScaling -> config.titleSegScaling = value
+            ReadStyleFloatKey.ShadowRadius -> config.shadowRadius = value
+            ReadStyleFloatKey.ShadowDx -> config.shadowDx = value
+            ReadStyleFloatKey.ShadowDy -> config.shadowDy = value
             ReadStyleFloatKey.DottedBase -> ReadBookConfig.durConfig.dottedBase = value
             ReadStyleFloatKey.DottedRatio -> ReadBookConfig.durConfig.dottedRatio = value
         }
     }
 
     private fun updateBoolean(key: ReadStyleBooleanKey, value: Boolean) {
+        val config = ReadBookConfig.config
         when (key) {
-            ReadStyleBooleanKey.TextItalic -> ReadBookConfig.textItalic = value
-            ReadStyleBooleanKey.TextShadow -> ReadBookConfig.textShadow = value
-            ReadStyleBooleanKey.Underline -> ReadBookConfig.underline = value
-            ReadStyleBooleanKey.DottedLine -> ReadBookConfig.dottedLine = value
-            ReadStyleBooleanKey.UnderlineExtend -> ReadBookConfig.underlineExtend = value
-            ReadStyleBooleanKey.ShowHeaderLine -> ReadBookConfig.showHeaderLine = value
-            ReadStyleBooleanKey.ShowFooterLine -> ReadBookConfig.showFooterLine = value
-            ReadStyleBooleanKey.ApplyHeaderStyle -> ReadBookConfig.applyHeaderStyle = value
+            ReadStyleBooleanKey.TextItalic -> config.textItalic = value
+            ReadStyleBooleanKey.TextShadow -> config.textShadow = value
+            ReadStyleBooleanKey.Underline -> config.underline = value
+            ReadStyleBooleanKey.DottedLine -> config.dottedLine = value
+            ReadStyleBooleanKey.UnderlineExtend -> config.underlineExtend = value
+            ReadStyleBooleanKey.ShowHeaderLine -> config.showHeaderLine = value
+            ReadStyleBooleanKey.ShowFooterLine -> config.showFooterLine = value
+            ReadStyleBooleanKey.ApplyHeaderStyle -> config.applyHeaderStyle = value
             ReadStyleBooleanKey.StatusIconDark -> ReadBookConfig.durConfig.setCurStatusIconDark(value)
         }
     }
 
     private fun updateString(key: ReadStyleStringKey, value: String) {
+        val config = ReadBookConfig.config
         when (key) {
-            ReadStyleStringKey.TextFont -> ReadBookConfig.textFont = value
-            ReadStyleStringKey.ParagraphIndent -> ReadBookConfig.paragraphIndent = value
-            ReadStyleStringKey.TitleFont -> ReadBookConfig.titleFont = value
-            ReadStyleStringKey.TitleSegFlag -> ReadBookConfig.titleSegFlag = value
-            ReadStyleStringKey.HeaderFont -> ReadBookConfig.headerFont = value
-            ReadStyleStringKey.FooterFont -> ReadBookConfig.footerFont = value
-            ReadStyleStringKey.CustomTipHeaderLeft -> ReadBookConfig.customTipHeaderLeft = value
-            ReadStyleStringKey.CustomTipHeaderMiddle -> ReadBookConfig.customTipHeaderMiddle = value
-            ReadStyleStringKey.CustomTipHeaderRight -> ReadBookConfig.customTipHeaderRight = value
-            ReadStyleStringKey.CustomTipFooterLeft -> ReadBookConfig.customTipFooterLeft = value
-            ReadStyleStringKey.CustomTipFooterMiddle -> ReadBookConfig.customTipFooterMiddle = value
-            ReadStyleStringKey.CustomTipFooterRight -> ReadBookConfig.customTipFooterRight = value
+            ReadStyleStringKey.TextFont -> config.textFont = value
+            ReadStyleStringKey.ParagraphIndent -> config.paragraphIndent = value
+            ReadStyleStringKey.TitleFont -> config.titleFont = value
+            ReadStyleStringKey.TitleSegFlag -> config.titleSegFlag = value
+            ReadStyleStringKey.HeaderFont -> config.headerFont = value
+            ReadStyleStringKey.FooterFont -> config.footerFont = value
+            ReadStyleStringKey.CustomTipHeaderLeft -> config.customTipHeaderLeft = value
+            ReadStyleStringKey.CustomTipHeaderMiddle -> config.customTipHeaderMiddle = value
+            ReadStyleStringKey.CustomTipHeaderRight -> config.customTipHeaderRight = value
+            ReadStyleStringKey.CustomTipFooterLeft -> config.customTipFooterLeft = value
+            ReadStyleStringKey.CustomTipFooterMiddle -> config.customTipFooterMiddle = value
+            ReadStyleStringKey.CustomTipFooterRight -> config.customTipFooterRight = value
             ReadStyleStringKey.BgStr -> ReadBookConfig.durConfig.bgStr = value
             ReadStyleStringKey.BgStrNight -> ReadBookConfig.durConfig.bgStrNight = value
             ReadStyleStringKey.BgStrEInk -> ReadBookConfig.durConfig.bgStrEInk = value
@@ -262,16 +266,17 @@ class ReadBookStyleConfigRepository(
     }
 
     private fun updateColor(key: ReadStyleColorKey, value: Int) {
+        val config = ReadBookConfig.config
         when (key) {
             ReadStyleColorKey.Text -> ReadBookConfig.durConfig.setCurTextColor(value)
             ReadStyleColorKey.TextAccent -> ReadBookConfig.durConfig.setCurTextAccentColor(value)
-            ReadStyleColorKey.Title -> ReadBookConfig.titleColor = value
-            ReadStyleColorKey.TitleNight -> ReadBookConfig.titleColorNight = value
-            ReadStyleColorKey.TipHeader -> ReadBookConfig.tipHeaderColor = value
-            ReadStyleColorKey.TipHeaderNight -> ReadBookConfig.tipHeaderColorNight = value
-            ReadStyleColorKey.TipFooter -> ReadBookConfig.tipFooterColor = value
-            ReadStyleColorKey.TipFooterNight -> ReadBookConfig.tipFooterColorNight = value
-            ReadStyleColorKey.TipDivider -> ReadBookConfig.tipDividerColor = value
+            ReadStyleColorKey.Title -> config.titleColor = value
+            ReadStyleColorKey.TitleNight -> config.titleColorNight = value
+            ReadStyleColorKey.TipHeader -> config.tipHeaderColor = value
+            ReadStyleColorKey.TipHeaderNight -> config.tipHeaderColorNight = value
+            ReadStyleColorKey.TipFooter -> config.tipFooterColor = value
+            ReadStyleColorKey.TipFooterNight -> config.tipFooterColorNight = value
+            ReadStyleColorKey.TipDivider -> config.tipDividerColor = value
             ReadStyleColorKey.Shadow -> ReadBookConfig.durConfig.setCurShadColor(value)
             ReadStyleColorKey.Underline -> ReadBookConfig.durConfig.setUnderlineColor(value)
         }

+ 75 - 300
app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt

@@ -272,346 +272,181 @@ object ReadBookConfig {
 
     val config get() = if (shareLayout) shareConfig else durConfig
 
-    var bgAlpha: Int
+    val bgAlpha: Int
         get() = config.bgAlpha
-        set(value) {
-            config.bgAlpha = value
-        }
 
-    var pageAnim: Int
+    val pageAnim: Int
         get() = config.curPageAnim()
-        set(@PageAnim.Anim value) {
-            config.setCurPageAnim(value)
-        }
 
-    var textFont: String
+    val textFont: String
         get() = config.textFont
-        set(value) {
-            config.textFont = value
-        }
 
-    var titleFont: String
+    val titleFont: String
         get() = config.titleFont
-        set(value) {
-            config.titleFont = value
-        }
 
-    var headerFont: String
+    val headerFont: String
         get() = config.headerFont
-        set(value) {
-            config.headerFont = value
-        }
 
-    var footerFont: String
+    val footerFont: String
         get() = config.footerFont
-        set(value) {
-            config.footerFont = value
-        }
 
-    var headerFontSize: Int
+    val headerFontSize: Int
         get() = config.headerFontSize.takeIf { it > 0 } ?: 12
-        set(value) {
-            config.headerFontSize = value
-        }
 
-    var footerFontSize: Int
+    val footerFontSize: Int
         get() = config.footerFontSize.takeIf { it > 0 } ?: 12
-        set(value) {
-            config.footerFontSize = value
-        }
 
-    var applyHeaderStyle: Boolean
+    val applyHeaderStyle: Boolean
         get() = config.applyHeaderStyle
-        set(value) {
-            config.applyHeaderStyle = value
-        }
 
-    var textBold: Int
+    val textBold: Int
         get() = config.textBold
-        set(value) {
-            config.textBold = value
-        }
 
-    var titleBold: Int
+    val titleBold: Int
         get() = config.titleBold
-        set(value) {
-            config.titleBold = value
-        }
 
-    var textItalic: Boolean
+    val textItalic: Boolean
         get() = config.textItalic
-        set(value) {
-            config.textItalic = value
-        }
 
-    var textShadow: Boolean
+    val textShadow: Boolean
         get() = config.textShadow
-        set(value) {
-            config.textShadow = value
-        }
 
-    var shadowRadius: Float
+    val shadowRadius: Float
         get() = config.shadowRadius
-        set(value) {
-            config.shadowRadius = value
-        }
 
-    var shadowDx: Float
+    val shadowDx: Float
         get() = config.shadowDx
-        set(value) {
-            config.shadowDx = value
-        }
 
-    var shadowDy: Float
+    val shadowDy: Float
         get() = config.shadowDy
-        set(value) {
-            config.shadowDy = value
-        }
 
-    var textSize: Int
+    val textSize: Int
         get() = config.textSize
-        set(value) {
-            config.textSize = value
-        }
 
-    var letterSpacing: Float
+    val letterSpacing: Float
         get() = config.letterSpacing
-        set(value) {
-            config.letterSpacing = value
-        }
 
-    var lineSpacingExtra: Int
+    val lineSpacingExtra: Int
         get() = config.lineSpacingExtra
-        set(value) {
-            config.lineSpacingExtra = value
-        }
 
-    var titleLineSpacingExtra: Int
+    val titleLineSpacingExtra: Int
         get() = config.titleLineSpacingExtra
-        set(value) {
-            config.titleLineSpacingExtra = value
-        }
 
-    var titleLineSpacingSub: Int
+    val titleLineSpacingSub: Int
         get() = config.titleLineSpacingSub
-        set(value) {
-            config.titleLineSpacingSub = value
-        }
 
-    var paragraphSpacing: Int
+    val paragraphSpacing: Int
         get() = config.paragraphSpacing
-        set(value) {
-            config.paragraphSpacing = value
-        }
 
     /**
      * 标题位置 0:居左 1:居中 2:隐藏
      */
-    var titleMode: Int
+    val titleMode: Int
         get() = config.titleMode
-        set(value) {
-            config.titleMode = value
-        }
-    var titleSize: Int
+    val titleSize: Int
         get() = config.titleSize
-        set(value) {
-            config.titleSize = value
-        }
 
-    var titleSegType: Int
+    val titleSegType: Int
         get() = config.titleSegType
-        set(value) {
-            config.titleSegType = value
-        }
 
-    var titleSegScaling: Float
+    val titleSegScaling: Float
         //旧版本可能存入负值,负值非法,回落到默认比例
         get() = config.titleSegScaling.let { if (it < 0f) 1f else it.coerceAtMost(2f) }
-        set(value) {
-            config.titleSegScaling = value
-        }
 
-    var titleSegDistance: Int
+    val titleSegDistance: Int
         get() = config.titleSegDistance
-        set(value) {
-            config.titleSegDistance = value
-        }
 
-    var titleSegFlag: String
+    val titleSegFlag: String
         get() = config.titleSegFlag
-        set(value) {
-            config.titleSegFlag = value
-        }
 
     /**
      * 是否标题居中
      */
     val isMiddleTitle get() = titleMode == 1
 
-    var titleTopSpacing: Int
+    val titleTopSpacing: Int
         get() = config.titleTopSpacing
-        set(value) {
-            config.titleTopSpacing = value
-        }
 
-    var titleBottomSpacing: Int
+    val titleBottomSpacing: Int
         get() = config.titleBottomSpacing
-        set(value) {
-            config.titleBottomSpacing = value
-        }
 
-    var titleColor: Int
+    val titleColor: Int
         get() = config.titleColor
-        set(value) {
-            config.titleColor = value
-        }
 
-    var titleColorNight: Int
+    val titleColorNight: Int
         get() = config.titleColorNight
-        set(value) {
-            config.titleColorNight = value
-        }
 
     val resolvedTitleColor: Int
         get() = if (ReadStyleResolver.isNightTheme()) titleColorNight else titleColor
 
-    var paragraphIndent: String
+    val paragraphIndent: String
         get() = config.paragraphIndent
-        set(value) {
-            config.paragraphIndent = value
-        }
 
-    var underline: Boolean
+    val underline: Boolean
         get() = config.underline
-        set(value) {
-            config.underline = value
-        }
 
-    var underlineHeight: Int
+    val underlineHeight: Int
         get() = config.underlineHeight
-        set(value) {
-            config.underlineHeight = value
-        }
 
-    var underlinePadding: Int
+    val underlinePadding: Int
         get() = config.underlinePadding
-        set(value) {
-            config.underlinePadding = value
-        }
 
-    var underlineExtend: Boolean
+    val underlineExtend: Boolean
         get() = config.underlineExtend
-        set(value) {
-            config.underlineExtend = value
-        }
 
-    var dottedLine: Boolean
+    val dottedLine: Boolean
         get() = config.dottedLine
-        set(value) {
-            config.dottedLine = value
-        }
 
-    var dottedBase: Float
+    val dottedBase: Float
         get() = config.dottedBase
-        set(value) {
-            config.dottedBase = value
-        }
 
-    var dottedRatio: Float
+    val dottedRatio: Float
         get() = config.dottedRatio
-        set(value) {
-            config.dottedRatio = value
-        }
 
-    var paddingBottom: Int
+    val paddingBottom: Int
         get() = config.paddingBottom
-        set(value) {
-            config.paddingBottom = value
-        }
 
-    var paddingLeft: Int
+    val paddingLeft: Int
         get() = config.paddingLeft
-        set(value) {
-            config.paddingLeft = value
-        }
 
-    var paddingRight: Int
+    val paddingRight: Int
         get() = config.paddingRight
-        set(value) {
-            config.paddingRight = value
-        }
 
-    var paddingTop: Int
+    val paddingTop: Int
         get() = config.paddingTop
-        set(value) {
-            config.paddingTop = value
-        }
 
-    var headerPaddingBottom: Int
+    val headerPaddingBottom: Int
         get() = config.headerPaddingBottom
-        set(value) {
-            config.headerPaddingBottom = value
-        }
 
-    var headerPaddingLeft: Int
+    val headerPaddingLeft: Int
         get() = config.headerPaddingLeft
-        set(value) {
-            config.headerPaddingLeft = value
-        }
 
-    var headerPaddingRight: Int
+    val headerPaddingRight: Int
         get() = config.headerPaddingRight
-        set(value) {
-            config.headerPaddingRight = value
-        }
 
-    var headerPaddingTop: Int
+    val headerPaddingTop: Int
         get() = config.headerPaddingTop
-        set(value) {
-            config.headerPaddingTop = value
-        }
 
-    var footerPaddingBottom: Int
+    val footerPaddingBottom: Int
         get() = config.footerPaddingBottom
-        set(value) {
-            config.footerPaddingBottom = value
-        }
 
-    var footerPaddingLeft: Int
+    val footerPaddingLeft: Int
         get() = config.footerPaddingLeft
-        set(value) {
-            config.footerPaddingLeft = value
-        }
 
-    var footerPaddingRight: Int
+    val footerPaddingRight: Int
         get() = config.footerPaddingRight
-        set(value) {
-            config.footerPaddingRight = value
-        }
 
-    var footerPaddingTop: Int
+    val footerPaddingTop: Int
         get() = config.footerPaddingTop
-        set(value) {
-            config.footerPaddingTop = value
-        }
 
-    var showHeaderLine: Boolean
+    val showHeaderLine: Boolean
         get() = config.showHeaderLine
-        set(value) {
-            config.showHeaderLine = value
-        }
 
-    var showFooterLine: Boolean
+    val showFooterLine: Boolean
         get() = config.showFooterLine
-        set(value) {
-            config.showFooterLine = value
-        }
 
-    var underlineColor: Int
+    val underlineColor: Int
         get() = config.curUnderlineColor()
-        set(value) {
-            config.setUnderlineColor(value)
-        }
 
     val menuBgColor: Int
         get() = readMenuBgColor
@@ -619,133 +454,73 @@ object ReadBookConfig {
     val menuAcColor: Int
         get() = readMenuAccentColor
 
-    var shadowColor: Int
+    val shadowColor: Int
         get() = config.curTextShadowColor()
-        set(value) {
-            config.setCurShadColor(value)
-        }
 
     // region Tip / Header / Footer
 
-    var tipHeaderLeft: Int
+    val tipHeaderLeft: Int
         get() = config.tipHeaderLeft
-        set(value) {
-            config.tipHeaderLeft = value
-        }
 
-    var tipHeaderMiddle: Int
+    val tipHeaderMiddle: Int
         get() = config.tipHeaderMiddle
-        set(value) {
-            config.tipHeaderMiddle = value
-        }
 
-    var tipHeaderRight: Int
+    val tipHeaderRight: Int
         get() = config.tipHeaderRight
-        set(value) {
-            config.tipHeaderRight = value
-        }
 
-    var tipFooterLeft: Int
+    val tipFooterLeft: Int
         get() = config.tipFooterLeft
-        set(value) {
-            config.tipFooterLeft = value
-        }
 
-    var tipFooterMiddle: Int
+    val tipFooterMiddle: Int
         get() = config.tipFooterMiddle
-        set(value) {
-            config.tipFooterMiddle = value
-        }
 
-    var tipFooterRight: Int
+    val tipFooterRight: Int
         get() = config.tipFooterRight
-        set(value) {
-            config.tipFooterRight = value
-        }
 
-    var customTipHeaderLeft: String
+    val customTipHeaderLeft: String
         get() = config.customTipHeaderLeft
-        set(value) {
-            config.customTipHeaderLeft = value
-        }
 
-    var customTipHeaderMiddle: String
+    val customTipHeaderMiddle: String
         get() = config.customTipHeaderMiddle
-        set(value) {
-            config.customTipHeaderMiddle = value
-        }
 
-    var customTipHeaderRight: String
+    val customTipHeaderRight: String
         get() = config.customTipHeaderRight
-        set(value) {
-            config.customTipHeaderRight = value
-        }
 
-    var customTipFooterLeft: String
+    val customTipFooterLeft: String
         get() = config.customTipFooterLeft
-        set(value) {
-            config.customTipFooterLeft = value
-        }
 
-    var customTipFooterMiddle: String
+    val customTipFooterMiddle: String
         get() = config.customTipFooterMiddle
-        set(value) {
-            config.customTipFooterMiddle = value
-        }
 
-    var customTipFooterRight: String
+    val customTipFooterRight: String
         get() = config.customTipFooterRight
-        set(value) {
-            config.customTipFooterRight = value
-        }
 
-    var headerMode: Int
+    val headerMode: Int
         get() = config.headerMode
-        set(value) {
-            config.headerMode = value
-        }
 
-    var footerMode: Int
+    val footerMode: Int
         get() = config.footerMode
-        set(value) {
-            config.footerMode = value
-        }
 
-    var tipHeaderColor: Int
+    val tipHeaderColor: Int
         get() = config.tipHeaderColor
-        set(value) {
-            config.tipHeaderColor = value
-        }
 
-    var tipHeaderColorNight: Int
+    val tipHeaderColorNight: Int
         get() = config.tipHeaderColorNight
-        set(value) {
-            config.tipHeaderColorNight = value
-        }
 
     val resolvedTipHeaderColor: Int
         get() = if (ReadStyleResolver.isNightTheme()) tipHeaderColorNight else tipHeaderColor
 
-    var tipFooterColor: Int
+    val tipFooterColor: Int
         get() = config.tipFooterColor
-        set(value) {
-            config.tipFooterColor = value
-        }
 
-    var tipFooterColorNight: Int
+    val tipFooterColorNight: Int
         get() = config.tipFooterColorNight
-        set(value) {
-            config.tipFooterColorNight = value
-        }
 
     val resolvedTipFooterColor: Int
         get() = if (ReadStyleResolver.isNightTheme()) tipFooterColorNight else tipFooterColor
 
-    var tipDividerColor: Int
+    val tipDividerColor: Int
         get() = config.tipDividerColor
-        set(value) {
-            config.tipDividerColor = value
-        }
 
     /** 与 `R.array.read_tip` 一一对应的展示顺序,故留在配置侧而不是 [ReadTipType]。 */
     val tipValues = with(ReadTipType) {

+ 31 - 0
app/src/test/java/io/legado/app/help/config/ReadBookConfigDependencyDirectionTest.kt

@@ -16,6 +16,11 @@ import java.io.File
  * 依赖 `ReadStyleRepository`(文件读写)。
  *
  * 注意 `readSettingsGateway` 不在此列——那是**只读**上游设置,不构成环。
+ *
+ * R4.4 又把写面整个搬进了 gateway 实现,`ReadBookConfig` 降为只读投影。这条不变式有两半,
+ * 分别由两处把守:**调用点**那半归 build.gradle.kts 的 `verifyConfigArchitecture`
+ * (「ReadBookConfig 写入必须经过 ReadStyleGateway」,编译期就拦);**声明点**那半归本文件
+ * ——文件内部写的是 `config.x = value`,绕不过那条正则,只能在这里断言。
  */
 class ReadBookConfigDependencyDirectionTest {
 
@@ -43,7 +48,33 @@ class ReadBookConfigDependencyDirectionTest {
         )
     }
 
+    @Test
+    fun `ReadBookConfig 是只读投影,没有公开可写属性`() {
+        val source = stripComments(readBookConfigSource())
+        val writable = Regex("""^    var (\w+)""", RegexOption.MULTILINE)
+            .findAll(source)
+            .map { it.groupValues[1] }
+            .filterNot { it in WRITABLE_ALLOWLIST }
+            .toList()
+
+        assertTrue(
+            "ReadBookConfig 又长出了公开可写属性:${writable.joinToString()}。\n" +
+                "排版值的写面只属于 ReadStyleGateway 的实现——加一个 setter," +
+                "就等于给全应用重新开了一条绕过 gateway 改配置、且不会触发 save/publishState " +
+                "的旁路,而且它是静默的:改了不落盘、订阅方收不到通知。\n" +
+                "新增可写排版项请加 ReadStyleMutation 的 key,在 " +
+                "ReadBookStyleConfigRepository 的 dispatch 里写 Config。",
+            writable.isEmpty(),
+        )
+    }
+
     private companion object {
+        /** `durConfig` 是整份配置的替换入口(应用预设 / 导入),只有 gateway 实现用得到。 */
+        val WRITABLE_ALLOWLIST = setOf("durConfig")
+
+        fun readBookConfigSource(): String =
+            mainSourceFile("io/legado/app/help/config/ReadBookConfig.kt").readText()
+
         fun stripComments(text: String): String = text
             .replace(Regex("""/\*[\s\S]*?\*/"""), "")
             .replace(Regex("""//[^\n]*"""), "")