ソースを参照

fix(rebase): 合上游后修复护栏、DAO 直连与弹层全局直读

rebase 到 upstream/main(28 提交)后,上游重写的屏幕带回了几处本项目已清零的
反模式,本提交把它们按既有约定重新收口:

- 上游新的 Compose 书源/RSS 源编辑与调试界面直连 appDb:四处改走
  BookSourceRepository / RssSourceEditRepository(Activity 用 by inject,
  两个 Debug VM 走构造注入)。build.gradle.kts 里三个已删文件的基线一并清掉,
  BookSourceActivity/BookSourceEditActivity 降到 0 后基线条目删除。
- 上游的 TypographyPage/TypographyTabs 吸收了页眉页脚设置,但把 E3 拆掉的
  19 个 `remember(ReadBookConfig.x)` 镜像本地态原样搬了回来——那正是
  「改完关掉再打开还是旧值」的根因。改为一律读 state.sheetConfig 快照,
  写入仍走 ConfigUpdate;sheet/ 下 ReadBookConfig 引用重新归零,
  R4.6 的护栏白名单得以真删。为此给 ReadSheetConfigUiState 补 textSize
  (标题字号 <8 时按「正文字号 + 偏移」解释)。
- PageView 的页脚字体覆盖分支直读全局,改由 TipStyleProvider 快照供给
  (新增 applyHeaderStyle/footerTypeface/footerTextSize,页脚字体单独缓存,
  避免与页眉字体路径互相打掉缓存、每实例主线程读字体文件)。
- 上游 useNewTocSheet(书籍信息/目录改开 Sheet)在 VM 里是两处复制粘贴,
  合并成 openBookNavigation()。
- 三条计数型护栏随上游新字段更新:ReadSettings 102→103(useNewTocSheet)、
  ThemeSettings 60→67(小容器背景七个键)、ReadBookViewModel 行数线 2500→2520
  (溢出来自 buildSheetConfig 投影表,非新域,理由记在测试注释里)。

验证:compileAppDebugKotlin + 481 单测 + verifyConfigArchitecture 全绿。
真机行为(页眉页脚设置、书源编辑保存、目录 Sheet)待验。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5151561 3 ヶ月 前
親
コミット
96581b87ff

+ 0 - 1
app/src/main/java/io/legado/app/data/repository/BookSourceRepository.kt

@@ -97,7 +97,6 @@ class BookSourceRepository(private val bookSourceDao: BookSourceDao) {
         }
     }
 
-
     suspend fun topSources(sources: List<BookSourcePart>) = withContext(Dispatchers.IO) {
         val minOrder = bookSourceDao.minOrder - 1
         val reorderedSources = sources.sortedBy { it.customOrder }.mapIndexed { index, source ->

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

@@ -105,6 +105,8 @@ data class ReadBookStyleConfig(
 
 @Stable
 data class ReadSheetConfigUiState(
+    /** 标题字号 <8 时按「正文字号 + 偏移」解释,故正文字号也要进弹层快照。 */
+    val textSize: Int = 20,
     val letterSpacing: Float = 0f,
     val lineSpacing: Int = 0,
     val paragraphSpacing: Int = 0,

+ 24 - 36
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -76,6 +76,7 @@ import io.legado.app.model.translation.TranslationChapterKey
 import io.legado.app.model.translation.TranslationChapterStatus
 import io.legado.app.model.translation.TranslationManager
 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.searchContent.SearchResult
 import io.legado.app.utils.ImageSaveUtils
@@ -169,6 +170,24 @@ class ReadBookViewModel(
         _effects.emit(effect)
     }
 
+    /**
+     * 书籍信息/目录有两种呈现:`useNewTocSheet` 开着时在阅读页内开 Sheet,
+     * 否则按老路子发 Effect 开新页。两个入口的判断完全一致,合在一处,
+     * 免得以后只改一边。
+     */
+    private fun openBookNavigation(
+        tab: ReaderBookSheetTab,
+        fallbackEffect: (Book) -> ReadBookEffect,
+    ) {
+        val book = ReadBook.book ?: return
+        closeReadMenu()
+        if (readSettingsRepository.currentSettings.useNewTocSheet) {
+            _uiState.update { it.copy(activeSheet = ReadBookSheet.BookNavigation(tab)) }
+        } else {
+            _effects.tryEmit(fallbackEffect(book))
+        }
+    }
+
     private fun closeReadMenu() {
         _uiState.update { it.copy(menuState = ReadBookMenuState()) }
     }
@@ -891,44 +910,12 @@ class ReadBookViewModel(
                     _effects.tryEmit(ReadBookEffect.OpenSourceEdit(src.bookSourceUrl))
                 }
             }
-            is ReadBookIntent.OpenBookInfo -> {
-                if (ReadBook.book != null) {
-                    closeReadMenu()
-                    if (readSettingsRepository.currentSettings.useNewTocSheet) {
-                        _uiState.update {
-                            it.copy(
-                                activeSheet = ReadBookSheet.BookNavigation(
-                                    io.legado.app.ui.book.read.sheet.ReaderBookSheetTab.Information
-                                )
-                            )
-                        }
-                    } else{
-                        ReadBook.book?.let { book ->
-                            _effects.tryEmit(ReadBookEffect.OpenBookInfo(book.name, book.author, book.bookUrl))
-                        }
-                    }
-
-                }
+            is ReadBookIntent.OpenBookInfo -> openBookNavigation(ReaderBookSheetTab.Information) {
+                ReadBookEffect.OpenBookInfo(it.name, it.author, it.bookUrl)
             }
-            is ReadBookIntent.OpenChapterList -> {
-                if (ReadBook.book != null) {
-                    closeReadMenu()
-                    if (readSettingsRepository.currentSettings.useNewTocSheet)
-                    {
-                        _uiState.update { state ->
-                            state.copy(
-                                activeSheet = ReadBookSheet.BookNavigation(
-                                    io.legado.app.ui.book.read.sheet.ReaderBookSheetTab.Toc
-                                )
-                            )
-                        }
-                    }else{
-                        ReadBook.book?.bookUrl?.let { bookUrl ->
-                            _effects.tryEmit(ReadBookEffect.OpenChapterList(bookUrl))
-                        }
-                    }
 
-                }
+            is ReadBookIntent.OpenChapterList -> openBookNavigation(ReaderBookSheetTab.Toc) {
+                ReadBookEffect.OpenChapterList(it.bookUrl)
             }
             is ReadBookIntent.OpenChapterUrl -> openChapterUrl()
             is ReadBookIntent.SourceCustomButton -> runSourceCustomButton(intent.longClick)
@@ -1744,6 +1731,7 @@ class ReadBookViewModel(
     }
 
     private fun buildSheetConfig(): ReadSheetConfigUiState = ReadSheetConfigUiState(
+        textSize = ReadBookConfig.textSize,
         letterSpacing = ReadBookConfig.letterSpacing,
         lineSpacing = ReadBookConfig.lineSpacingExtra,
         paragraphSpacing = ReadBookConfig.paragraphSpacing,

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

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

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

@@ -60,6 +60,10 @@ internal object TipStyleProvider {
         val footerPaddingTop: Int = 0,
         val footerPaddingRight: Int = 0,
         val footerPaddingBottom: Int = 0,
+        val applyHeaderStyle: Boolean = true,
+        /** 已按 `footerFont` 解析;仅在 [applyHeaderStyle] 为 false 时用得上。 */
+        val footerTypeface: Typeface? = null,
+        val footerTextSize: Float = 0f,
         val showHeaderLine: Boolean = false,
         val showFooterLine: Boolean = false,
         val bgAlpha: Int = 100,
@@ -113,6 +117,9 @@ internal object TipStyleProvider {
             customTipFooterRight = customTipFooterRight,
             tipTypeface = typefaceFor(headerFont),
             tipTextSize = headerFontSize.toFloat(),
+            applyHeaderStyle = applyHeaderStyle,
+            footerTypeface = footerTypefaceFor(footerFont),
+            footerTextSize = footerFontSize.toFloat(),
             textColor = textColor,
             tipHeaderColor = resolvedTipHeaderColor,
             tipFooterColor = resolvedTipFooterColor,
@@ -133,6 +140,8 @@ internal object TipStyleProvider {
 
     private var cachedFontPath: String? = null
     private var cachedTypeface: Typeface? = null
+    private var cachedFooterFontPath: String? = null
+    private var cachedFooterTypeface: Typeface? = null
 
     /** 按路径缓存:字体文件解析是主线程 I/O,只有路径变了才值得重做。 */
     private fun typefaceFor(fontPath: String): Typeface? {
@@ -143,6 +152,15 @@ internal object TipStyleProvider {
         return cachedTypeface
     }
 
+    /** 页脚字体单独缓存:与页眉字体路径不同,共用一个槽位会互相打掉缓存。 */
+    private fun footerTypefaceFor(fontPath: String): Typeface? {
+        if (fontPath != cachedFooterFontPath) {
+            cachedFooterFontPath = fontPath
+            cachedFooterTypeface = loadTypeface(fontPath)
+        }
+        return cachedFooterTypeface
+    }
+
     private fun loadTypeface(fontPath: String): Typeface? = runCatching {
         when {
             fontPath.isContentScheme() -> appCtx.contentResolver

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

@@ -26,7 +26,7 @@ import androidx.compose.ui.layout.onSizeChanged
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
-import io.legado.app.help.config.ReadBookConfig
+import io.legado.app.constant.ReadTipType
 import io.legado.app.ui.book.read.ConfigUpdate
 import io.legado.app.ui.book.read.ReadBookIntent
 import io.legado.app.ui.book.read.ReadSheetConfigUiState
@@ -73,79 +73,49 @@ fun TypographyPage(
     var activeFooterFontSelect by remember { mutableStateOf(false) }
     var activeCustomTip by remember { mutableStateOf<CustomTipTarget?>(null) }
 
-    // Lifted tip state — shared between Header Tab and Footer Tab for cross-tab clearRepeat.
-    // Keys ensure re-initialization when ReadBookConfig changes from external sources.
-    var headerMode by remember(ReadBookConfig.headerMode) { mutableIntStateOf(ReadBookConfig.headerMode) }
-    var headerLeft by remember(ReadBookConfig.tipHeaderLeft) { mutableIntStateOf(ReadBookConfig.tipHeaderLeft) }
-    var headerMiddle by remember(ReadBookConfig.tipHeaderMiddle) { mutableIntStateOf(ReadBookConfig.tipHeaderMiddle) }
-    var headerRight by remember(ReadBookConfig.tipHeaderRight) { mutableIntStateOf(ReadBookConfig.tipHeaderRight) }
-    var footerMode by remember(ReadBookConfig.footerMode) { mutableIntStateOf(ReadBookConfig.footerMode) }
-    var footerLeft by remember(ReadBookConfig.tipFooterLeft) { mutableIntStateOf(ReadBookConfig.tipFooterLeft) }
-    var footerMiddle by remember(ReadBookConfig.tipFooterMiddle) { mutableIntStateOf(ReadBookConfig.tipFooterMiddle) }
-    var footerRight by remember(ReadBookConfig.tipFooterRight) { mutableIntStateOf(ReadBookConfig.tipFooterRight) }
-    var showHeaderLine by remember(ReadBookConfig.showHeaderLine) { mutableStateOf(ReadBookConfig.showHeaderLine) }
-    var showFooterLine by remember(ReadBookConfig.showFooterLine) { mutableStateOf(ReadBookConfig.showFooterLine) }
-
+    /**
+     * 同一个 tip 只能占一个位置:换到新位置前先把旧位置清空。
+     *
+     * 位置取值一律从 [config] 快照读,不镜像本地 state——镜像进 `remember` 只在首次组合
+     * 读一次,上游改动(另一 Tab、外部改配置)不会重新 seed,弹层就会显示旧值。
+     */
     fun clearRepeat(repeat: Int) {
-        if (repeat == ReadBookConfig.tipNone) return
-        if (headerLeft == repeat) {
-            headerLeft = ReadBookConfig.tipNone
-            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderLeft(ReadBookConfig.tipNone)))
+        if (repeat == ReadTipType.tipNone) return
+        if (config.tipHeaderLeft == repeat) {
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderLeft(ReadTipType.tipNone)))
         }
-        if (headerMiddle == repeat) {
-            headerMiddle = ReadBookConfig.tipNone
-            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderMiddle(ReadBookConfig.tipNone)))
+        if (config.tipHeaderMiddle == repeat) {
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderMiddle(ReadTipType.tipNone)))
         }
-        if (headerRight == repeat) {
-            headerRight = ReadBookConfig.tipNone
-            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderRight(ReadBookConfig.tipNone)))
+        if (config.tipHeaderRight == repeat) {
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderRight(ReadTipType.tipNone)))
         }
-        if (footerLeft == repeat) {
-            footerLeft = ReadBookConfig.tipNone
-            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterLeft(ReadBookConfig.tipNone)))
+        if (config.tipFooterLeft == repeat) {
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterLeft(ReadTipType.tipNone)))
         }
-        if (footerMiddle == repeat) {
-            footerMiddle = ReadBookConfig.tipNone
-            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterMiddle(ReadBookConfig.tipNone)))
+        if (config.tipFooterMiddle == repeat) {
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterMiddle(ReadTipType.tipNone)))
         }
-        if (footerRight == repeat) {
-            footerRight = ReadBookConfig.tipNone
-            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterRight(ReadBookConfig.tipNone)))
+        if (config.tipFooterRight == repeat) {
+            onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterRight(ReadTipType.tipNone)))
         }
     }
 
     fun handleTipChange(target: CustomTipTarget, value: Int) {
-        if (value == ReadBookConfig.tipCustom) {
+        if (value == ReadTipType.tipCustom) {
             activeCustomTip = target
             return
         }
         clearRepeat(value)
-        when (target) {
-            CustomTipTarget.HEADER_LEFT -> {
-                headerLeft = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderLeft(value)))
-            }
-            CustomTipTarget.HEADER_MIDDLE -> {
-                headerMiddle = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderMiddle(value)))
-            }
-            CustomTipTarget.HEADER_RIGHT -> {
-                headerRight = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipHeaderRight(value)))
-            }
-            CustomTipTarget.FOOTER_LEFT -> {
-                footerLeft = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterLeft(value)))
-            }
-            CustomTipTarget.FOOTER_MIDDLE -> {
-                footerMiddle = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterMiddle(value)))
-            }
-            CustomTipTarget.FOOTER_RIGHT -> {
-                footerRight = value
-                onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.TipFooterRight(value)))
-            }
+        val update = when (target) {
+            CustomTipTarget.HEADER_LEFT -> ConfigUpdate.TipHeaderLeft(value)
+            CustomTipTarget.HEADER_MIDDLE -> ConfigUpdate.TipHeaderMiddle(value)
+            CustomTipTarget.HEADER_RIGHT -> ConfigUpdate.TipHeaderRight(value)
+            CustomTipTarget.FOOTER_LEFT -> ConfigUpdate.TipFooterLeft(value)
+            CustomTipTarget.FOOTER_MIDDLE -> ConfigUpdate.TipFooterMiddle(value)
+            CustomTipTarget.FOOTER_RIGHT -> ConfigUpdate.TipFooterRight(value)
         }
+        onIntent(ReadBookIntent.UpdateConfig(update))
     }
 
     val childPagerNestedScrollConnection = rememberPagerFlingPassThroughConnection(
@@ -222,18 +192,16 @@ fun TypographyPage(
                         onOpenHeaderFontSelect = { activeHeaderFontSelect = true },
                         onOpenColorPicker = { activeColorPicker = it },
                         onOpenCustomTip = { target: CustomTipTarget -> activeCustomTip = target },
-                        headerMode = headerMode,
-                        headerLeft = headerLeft,
-                        headerMiddle = headerMiddle,
-                        headerRight = headerRight,
+                        headerMode = config.headerMode,
+                        headerLeft = config.tipHeaderLeft,
+                        headerMiddle = config.tipHeaderMiddle,
+                        headerRight = config.tipHeaderRight,
                         onHeaderModeChange = {
-                            headerMode = it
                             onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderMode(it)))
                         },
                         onTipChange = ::handleTipChange,
-                        showHeaderLine = showHeaderLine,
+                        showHeaderLine = config.showHeaderLine,
                         onShowHeaderLineChange = {
-                            showHeaderLine = it
                             onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShowHeaderLine(it)))
                         },
                         modifier = Modifier.padding(horizontal = 16.dp),
@@ -245,18 +213,16 @@ fun TypographyPage(
                         onOpenFooterFontSelect = { activeFooterFontSelect = true },
                         onOpenColorPicker = { activeColorPicker = it },
                         onOpenCustomTip = { target: CustomTipTarget -> activeCustomTip = target },
-                        footerMode = footerMode,
-                        footerLeft = footerLeft,
-                        footerMiddle = footerMiddle,
-                        footerRight = footerRight,
+                        footerMode = config.footerMode,
+                        footerLeft = config.tipFooterLeft,
+                        footerMiddle = config.tipFooterMiddle,
+                        footerRight = config.tipFooterRight,
                         onFooterModeChange = {
-                            footerMode = it
                             onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterMode(it)))
                         },
                         onTipChange = ::handleTipChange,
-                        showFooterLine = showFooterLine,
+                        showFooterLine = config.showFooterLine,
                         onShowFooterLineChange = {
-                            showFooterLine = it
                             onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.ShowFooterLine(it)))
                         },
                         modifier = Modifier.padding(horizontal = 16.dp),
@@ -306,6 +272,7 @@ fun TypographyPage(
     activeCustomTip?.let { target ->
         TypographyCustomTipDialog(
             target = target,
+            config = config,
             onDismiss = { activeCustomTip = null },
             onIntent = onIntent,
         )

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

@@ -430,7 +430,7 @@ internal fun TypographyTitleTab(
     // Title font size: <8 means relative offset (textSize + titleSize), >=8 means absolute
     var titleSize by remember(config.titleSize) {
         val initial = if (config.titleSize < 8) {
-            (ReadBookConfig.textSize + config.titleSize).coerceIn(8, 60)
+            (config.textSize + config.titleSize).coerceIn(8, 60)
         } else {
             config.titleSize
         }
@@ -677,7 +677,7 @@ internal fun TypographyHeaderTab(
             title = stringResource(R.string.tip_divider_color),
             colorValue = when (config.tipDividerColor) {
                 -1 -> context.getCompatColor(R.color.divider)
-                0 -> ReadBookConfig.textColor
+                0 -> config.textColorDay
                 else -> config.tipDividerColor
             },
             onClick = { onOpenColorPicker(TypographyColorTarget.Divider) },
@@ -1028,7 +1028,7 @@ internal fun TypographyHeaderFontSelectSheet(
     onIntent: (ReadBookIntent) -> Unit,
 ) {
     TypographyFontSelectSheet(
-        selectedFontPath = ReadBookConfig.headerFont,
+        selectedFontPath = config.headerFont,
         onSelectFont = { fileDoc ->
             onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.HeaderFont(fileDoc.uri.toString())))
             onIntent(ReadBookIntent.SaveReadStyleConfig)
@@ -1050,7 +1050,7 @@ internal fun TypographyFooterFontSelectSheet(
     onIntent: (ReadBookIntent) -> Unit,
 ) {
     TypographyFontSelectSheet(
-        selectedFontPath = ReadBookConfig.footerFont,
+        selectedFontPath = config.footerFont,
         onSelectFont = { fileDoc ->
             onIntent(ReadBookIntent.UpdateConfig(ConfigUpdate.FooterFont(fileDoc.uri.toString())))
             onIntent(ReadBookIntent.SaveReadStyleConfig)
@@ -1068,16 +1068,17 @@ internal fun TypographyFooterFontSelectSheet(
 @Composable
 internal fun TypographyCustomTipDialog(
     target: CustomTipTarget,
+    config: ReadSheetConfigUiState,
     onDismiss: () -> Unit,
     onIntent: (ReadBookIntent) -> Unit,
 ) {
-    val initialTemplate = remember(target) { target.customTemplate }
+    val initialTemplate = target.customTemplateOf(config)
 
     CustomTipDialog(
         show = true,
         initialTemplate = initialTemplate,
         onConfirm = { template ->
-            applyTipValue(target, ReadBookConfig.tipCustom, onIntent)
+            applyTipValue(target, ReadTipType.tipCustom, onIntent)
             target.applyTemplate(template, onIntent)
             onDismiss()
         },

+ 6 - 3
app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugViewModel.kt

@@ -3,8 +3,8 @@ package io.legado.app.ui.book.source.debug
 import android.app.Application
 import androidx.lifecycle.AndroidViewModel
 import androidx.lifecycle.viewModelScope
-import io.legado.app.data.appDb
 import io.legado.app.data.entities.BookSource
+import io.legado.app.data.repository.BookSourceRepository
 import io.legado.app.help.source.exploreKinds
 import io.legado.app.model.Debug
 import kotlinx.collections.immutable.toImmutableList
@@ -19,7 +19,10 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import java.util.concurrent.atomic.AtomicLong
 
-class BookSourceDebugViewModel(application: Application) : AndroidViewModel(application) {
+class BookSourceDebugViewModel(
+    application: Application,
+    private val repository: BookSourceRepository,
+) : AndroidViewModel(application) {
     private val _uiState = MutableStateFlow(BookSourceDebugUiState())
     val uiState = _uiState.asStateFlow()
     private val _effects = MutableSharedFlow<BookSourceDebugEffect>(extraBufferCapacity = 16)
@@ -47,7 +50,7 @@ class BookSourceDebugViewModel(application: Application) : AndroidViewModel(appl
 
     private fun load(sourceUrl: String?) = viewModelScope.launch {
         val loaded: BookSource? = withContext(Dispatchers.IO) {
-            sourceUrl?.let { appDb.bookSourceDao.getBookSource(it) }
+            sourceUrl?.let { repository.getBookSource(it) }
         }
         source = loaded
         if (loaded == null) {

+ 6 - 3
app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt

@@ -11,7 +11,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.lifecycleScope
 import io.legado.app.R
 import io.legado.app.base.BaseComposeActivity
-import io.legado.app.data.appDb
+import io.legado.app.data.repository.BookSourceRepository
 import io.legado.app.data.entities.BookSource
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.ui.book.search.SearchActivity
@@ -31,10 +31,13 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import org.koin.android.ext.android.inject
 import org.koin.androidx.compose.koinViewModel
 
 class BookSourceEditActivity : BaseComposeActivity(), VariableDialog.Callback {
 
+    private val sourceRepository by inject<BookSourceRepository>()
+
     @Composable
     override fun Content() {
         val viewModel = koinViewModel<BookSourceEditViewModel>()
@@ -104,7 +107,7 @@ class BookSourceEditActivity : BaseComposeActivity(), VariableDialog.Callback {
     private fun openVariable(sourceUrl: String) {
         lifecycleScope.launch {
             val source =
-                withContext(Dispatchers.IO) { appDb.bookSourceDao.getBookSource(sourceUrl) }
+                sourceRepository.getBookSource(sourceUrl)
                     ?: return@launch
             val comment =
                 source.getDisplayVariableComment("源变量可在js中通过source.getVariable()获取")
@@ -122,7 +125,7 @@ class BookSourceEditActivity : BaseComposeActivity(), VariableDialog.Callback {
 
     override fun setVariable(key: String, variable: String?) {
         lifecycleScope.launch(Dispatchers.IO) {
-            appDb.bookSourceDao.getBookSource(key)?.setVariable(variable)
+            sourceRepository.getBookSource(key)?.setVariable(variable)
         }
     }
 }

+ 6 - 3
app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugViewModel.kt

@@ -3,8 +3,8 @@ package io.legado.app.ui.rss.source.debug
 import android.app.Application
 import androidx.lifecycle.AndroidViewModel
 import androidx.lifecycle.viewModelScope
-import io.legado.app.data.appDb
 import io.legado.app.data.entities.RssSource
+import io.legado.app.data.repository.RssSourceEditRepository
 import io.legado.app.help.source.sortUrls
 import io.legado.app.model.Debug
 import io.legado.app.ui.book.source.debug.BookSourceDebugEntryUi
@@ -17,7 +17,10 @@ import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import java.util.concurrent.atomic.AtomicLong
 
-class RssSourceDebugViewModel(app: Application) : AndroidViewModel(app) {
+class RssSourceDebugViewModel(
+    app: Application,
+    private val repository: RssSourceEditRepository,
+) : AndroidViewModel(app) {
     private val _uiState = MutableStateFlow(RssSourceDebugUiState())
     val uiState = _uiState.asStateFlow()
     private val _effects = MutableSharedFlow<RssSourceDebugEffect>(extraBufferCapacity = 16)
@@ -40,7 +43,7 @@ class RssSourceDebugViewModel(app: Application) : AndroidViewModel(app) {
     }
 
     private fun load(url: String?) = viewModelScope.launch {
-        val loaded = withContext(Dispatchers.IO) { url?.let { appDb.rssSourceDao.getByKey(it) } }
+        val loaded = url?.let { repository.findByUrl(it) }
         source = loaded
         if (loaded == null) {
             _uiState.update { it.copy(status = BookSourceDebugStatus.Failed) }

+ 5 - 3
app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt

@@ -7,7 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.lifecycleScope
 import io.legado.app.R
 import io.legado.app.base.BaseComposeActivity
-import io.legado.app.data.appDb
+import io.legado.app.data.repository.RssSourceEditRepository
 import io.legado.app.ui.login.SourceLoginActivity
 import io.legado.app.ui.rss.source.debug.RssSourceDebugActivity
 import io.legado.app.ui.widget.dialog.VariableDialog
@@ -21,9 +21,11 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import org.koin.android.ext.android.inject
 import org.koin.androidx.compose.koinViewModel
 
 class RssSourceEditActivity : BaseComposeActivity(), VariableDialog.Callback {
+ private val sourceRepository by inject<RssSourceEditRepository>()
  @Composable override fun Content(){val vm=koinViewModel<RssSourceEditViewModel>();val state=vm.uiState.collectAsStateWithLifecycle().value
   LaunchedEffect(Unit){vm.onIntent(RssSourceEditIntent.Load(intent.getStringExtra("sourceUrl")));vm.effects.collectLatest{e->when(e){
    is RssSourceEditEffect.Finish->{if(e.url.isNotEmpty())setResult(RESULT_OK,Intent().putExtra("origin",e.url));finish()}
@@ -36,6 +38,6 @@ class RssSourceEditActivity : BaseComposeActivity(), VariableDialog.Callback {
   }}}
   RssSourceEditScreen(state,vm::onIntent){vm.onIntent(RssSourceEditIntent.Back)}
  }
- private fun openVariable(url:String)=lifecycleScope.launch{val s=withContext(Dispatchers.IO){appDb.rssSourceDao.getByKey(url)}?:return@launch;showDialogFragment(VariableDialog(getString(R.string.set_source_variable),s.getKey(),withContext(Dispatchers.IO){s.getVariable()},s.getDisplayVariableComment("源变量可在js中通过source.getVariable()获取")))}
- override fun setVariable(key:String,variable:String?){lifecycleScope.launch(Dispatchers.IO){appDb.rssSourceDao.getByKey(key)?.setVariable(variable)}}
+ private fun openVariable(url:String)=lifecycleScope.launch{val s=sourceRepository.findByUrl(url)?:return@launch;showDialogFragment(VariableDialog(getString(R.string.set_source_variable),s.getKey(),withContext(Dispatchers.IO){s.getVariable()},s.getDisplayVariableComment("源变量可在js中通过source.getVariable()获取")))}
+ override fun setVariable(key:String,variable:String?){lifecycleScope.launch(Dispatchers.IO){sourceRepository.findByUrl(key)?.setVariable(variable)}}
 }

+ 4 - 2
app/src/test/java/io/legado/app/data/repository/ReadSettingsMappingTest.kt

@@ -14,11 +14,11 @@ import kotlin.reflect.full.primaryConstructor
 class ReadSettingsMappingTest {
 
     @Test
-    fun `gateway 持久化映射覆盖 ReadSettings 全部 102 个字段`() {
+    fun `gateway 持久化映射覆盖 ReadSettings 全部 103 个字段`() {
         val actualKeys = ReadSettings().toGatewayPrefMap().keys
         val expectedKeys = ReadSettings().expectedGatewayPrefMap().keys
 
-        assertEquals(102, actualKeys.size)
+        assertEquals(103, actualKeys.size)
         assertEquals(expectedKeys, actualKeys)
     }
 
@@ -132,6 +132,7 @@ private fun readSettingsMappingSamples(): List<ReadSettings> {
         autoReadSpeed = 1004,
         tocUiUseReplace = true,
         tocCountWords = false,
+        useNewTocSheet = false,
         readStyleSelect = 1007,
         comicStyleSelect = 1008,
         shareLayout = true,
@@ -263,6 +264,7 @@ private fun ReadSettings.expectedGatewayPrefMap(): Map<String, Any?> = mapOf(
     PreferKey.nextKeys to nextKeys,
     PreferKey.tocUiUseReplace to tocUiUseReplace,
     PreferKey.tocCountWords to tocCountWords,
+    PreferKey.useNewTocSheet to useNewTocSheet,
     PreferKey.readUrlOpenInBrowser to readUrlInBrowser,
     PreferKey.readStyleSelect to readStyleSelect,
     PreferKey.comicStyleSelect to comicStyleSelect,

+ 17 - 3
app/src/test/java/io/legado/app/data/repository/ThemeSettingsMappingTest.kt

@@ -27,18 +27,18 @@ class ThemeSettingsMappingTest {
     }
 
     @Test
-    fun `Theme gateway 持久化边界固定为 60 键`() {
+    fun `Theme gateway 持久化边界固定为 67 键`() {
         val actualKeys = ThemeSettings().toGatewayPrefMap().keys
         val expectedKeys = ThemeSettings().expectedGatewayPrefMap().keys
 
-        assertEquals(60, actualKeys.size)
+        assertEquals(67, actualKeys.size)
         assertEquals(expectedKeys, actualKeys)
         assertFalse(PreferKey.customMode in actualKeys)
         assertFalse(PreferKey.bookInfoInputColor in actualKeys)
     }
 
     @Test
-    fun `Theme gateway 60 键写读映射逐字段对应`() {
+    fun `Theme gateway 67 键写读映射逐字段对应`() {
         themeMappingSamples().forEach { expected ->
             assertEquals(expected.expectedGatewayPrefMap(), expected.toGatewayPrefMap())
             assertEquals(
@@ -194,6 +194,13 @@ private fun themeMappingSamples(): List<ThemeSettings> {
         backgroundImageDark = "dark-background",
         backgroundImageBlurring = 124,
         backgroundImageDarkBlurring = 125,
+        largeContainerBackgroundImageLight = "large-container-light",
+        largeContainerBackgroundImageDark = "large-container-dark",
+        itemBackgroundImageLight = "item-light",
+        itemBackgroundImageDark = "item-dark",
+        enableContainerBackgroundImage = true,
+        appColumnBackgroundOpacity = 127,
+        glassCardBackgroundOpacity = 128,
         enableItemDivider = false,
         itemDividerWidth = 4.5f,
         itemDividerLength = 5.75f,
@@ -277,6 +284,13 @@ private fun ThemeSettings.expectedGatewayPrefMap(): Map<String, Any?> = mapOf(
     PreferKey.bgImageN to backgroundImageDark,
     PreferKey.bgImageBlurring to backgroundImageBlurring,
     PreferKey.bgImageNBlurring to backgroundImageDarkBlurring,
+    PreferKey.largeContainerBackgroundImageLight to largeContainerBackgroundImageLight,
+    PreferKey.largeContainerBackgroundImageDark to largeContainerBackgroundImageDark,
+    PreferKey.itemBackgroundImageLight to itemBackgroundImageLight,
+    PreferKey.itemBackgroundImageDark to itemBackgroundImageDark,
+    PreferKey.enableContainerBackgroundImage to enableContainerBackgroundImage,
+    PreferKey.appColumnBackgroundOpacity to appColumnBackgroundOpacity,
+    PreferKey.glassCardBackgroundOpacity to glassCardBackgroundOpacity,
     PreferKey.enableItemDivider to enableItemDivider,
     PreferKey.itemDividerWidth to itemDividerWidth,
     PreferKey.itemDividerLength to itemDividerLength,

+ 9 - 3
app/src/test/java/io/legado/app/ui/book/read/ReadBookDomainSplitBoundaryTest.kt

@@ -62,16 +62,22 @@ class ReadBookDomainSplitBoundaryTest {
     /**
      * R2 的终态验收线。不是为了追行数好看——超过这个数就说明又有新的域直接长在 VM 里,
      * 而不是长成一个 delegate。要放宽必须先说明新增的是哪个域、为什么不能摘。
+     *
+     * 2500 → 2520:合上游后放宽 20 行。溢出的不是新域,是 `buildSheetConfig()` 这张
+     * 投影表——上游给页眉页脚加了字体/字号/`applyHeaderStyle`/`tipDividerColor`,
+     * 再加两个对齐项,一个字段就是一行,纯派生、没有逻辑可摘。上游同批带来的
+     * `useNewTocSheet` 分支(书籍信息/目录改开 Sheet)本来是两处复制粘贴,
+     * 已合并成 `openBookNavigation()`,那部分没占额度。
      */
     @Test
-    fun `ReadBookViewModel 不超过 R2 验收的 2500 行`() {
+    fun `ReadBookViewModel 不超过 R2 验收的 2520 行`() {
         val lineCount = mainSourceFile("io/legado/app/ui/book/read/ReadBookViewModel.kt")
             .readLines().size
         assertTrue(
-            "ReadBookViewModel 涨到了 $lineCount 行,超过 R2 验收线 2500。\n" +
+            "ReadBookViewModel 涨到了 $lineCount 行,超过 R2 验收线 2520。\n" +
                 "新功能请摘成 io/legado/app/ui/book/read/ 下的 XxxDelegate," +
                 "并在本测试的 DOMAINS 里加一条边界。",
-            lineCount <= 2500,
+            lineCount <= 2520,
         )
     }
 

+ 0 - 5
build.gradle.kts

@@ -285,15 +285,10 @@ val verifyConfigArchitecture = tasks.register<VerifyConfigArchitectureTask>(
             "io/legado/app/ui/book/read/pageestimate/ExactChapterPageCountStore.kt" to 3,
             "io/legado/app/ui/book/search/SearchScope.kt" to 4,
             "io/legado/app/ui/book/search/SearchScopeDialog.kt" to 3,
-            "io/legado/app/ui/book/source/debug/BookSourceDebugModel.kt" to 1,
-            "io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt" to 1,
-            "io/legado/app/ui/book/source/manage/BookSourceActivity.kt" to 10,
-            "io/legado/app/ui/book/source/manage/GroupManageDialog.kt" to 1,
             "io/legado/app/ui/config/bookshelfConfig/BookshelfManageScreenConfig.kt" to 1,
             "io/legado/app/ui/main/MainNavGraph.kt" to 2,
             "io/legado/app/ui/rss/article/RssArticlesCompose.kt" to 1,
             "io/legado/app/ui/rss/read/RssJsExtensions.kt" to 8,
-            "io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt" to 1,
             "io/legado/app/ui/widget/dialog/BottomWebViewDialog.kt" to 1,
             "io/legado/app/ui/widget/keyboard/KeyboardAssistsConfig.kt" to 7,
             "io/legado/app/ui/widget/keyboard/KeyboardToolPop.kt" to 1,