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

refactor: 迁移 Theme 设置网关

5151561 3 месяцев назад
Родитель
Сommit
0419be1da5

+ 73 - 94
app/src/main/java/io/legado/app/data/repository/FeatureSettingsRepositories.kt

@@ -9,13 +9,7 @@ import io.legado.app.domain.gateway.CoverSettingsGateway
 import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.domain.gateway.LabSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
-import io.legado.app.domain.gateway.ThemeBooleanSetting
-import io.legado.app.domain.gateway.ThemeColorSlot
-import io.legado.app.domain.gateway.ThemeFloatSetting
-import io.legado.app.domain.gateway.ThemeIntSetting
 import io.legado.app.domain.gateway.ThemeSettingsGateway
-import io.legado.app.domain.gateway.ThemeSettingsUpdate
-import io.legado.app.domain.gateway.ThemeStringSetting
 import io.legado.app.domain.gateway.TranslationSettingsGateway
 import io.legado.app.domain.model.TranslationConstants
 import io.legado.app.domain.model.settings.AppShellSettings
@@ -60,94 +54,12 @@ class ThemeSettingsRepository : ThemeSettingsGateway {
         .map(Preferences::toThemeSettings)
         .distinctUntilChanged()
 
-    override suspend fun update(update: ThemeSettingsUpdate) = updateAll(listOf(update))
-
-    override suspend fun updateAll(updates: List<ThemeSettingsUpdate>) {
-        val values = updates.associate { update ->
-            val (key, value) = when (update) {
-            is ThemeSettingsUpdate.AppTheme -> PreferKey.appTheme to update.value
-            is ThemeSettingsUpdate.PureBlack -> PreferKey.pureBlack to update.value
-            is ThemeSettingsUpdate.PaletteStyle -> PreferKey.paletteStyle to update.value
-            is ThemeSettingsUpdate.MaterialVersion -> PreferKey.materialVersion to update.value
-            is ThemeSettingsUpdate.CustomContrast -> PreferKey.customContrast to update.value
-            is ThemeSettingsUpdate.DeepPersonalization ->
-                PreferKey.enableDeepPersonalization to update.value
-            is ThemeSettingsUpdate.CustomColor -> when (update.slot) {
-                ThemeColorSlot.Primary -> PreferKey.themeColor to update.value
-                ThemeColorSlot.Secondary -> PreferKey.secondaryThemeColor to update.value
-                ThemeColorSlot.PrimaryText -> PreferKey.primaryTextColor to update.value
-                ThemeColorSlot.SecondaryText -> PreferKey.secondaryTextColor to update.value
-                ThemeColorSlot.Background -> PreferKey.themeBackgroundColor to update.value
-                ThemeColorSlot.LabelContainer -> PreferKey.labelContainerColor to update.value
-                ThemeColorSlot.PrimaryNight -> PreferKey.themeColorNight to update.value
-                ThemeColorSlot.SecondaryNight -> PreferKey.secondaryThemeColorNight to update.value
-                ThemeColorSlot.PrimaryTextNight -> PreferKey.primaryTextColorNight to update.value
-                ThemeColorSlot.SecondaryTextNight -> PreferKey.secondaryTextColorNight to update.value
-                ThemeColorSlot.BackgroundNight -> PreferKey.themeBackgroundColorNight to update.value
-                ThemeColorSlot.LabelContainerNight -> PreferKey.labelContainerColorNight to update.value
-            }
-            is ThemeSettingsUpdate.AppFontPath -> PreferKey.appFontPath to update.value
-            is ThemeSettingsUpdate.CustomPrimary -> PreferKey.cPrimary to update.value
-            is ThemeSettingsUpdate.CustomNightPrimary -> PreferKey.cNPrimary to update.value
-            is ThemeSettingsUpdate.BooleanValue -> when (update.setting) {
-                ThemeBooleanSetting.UseMiuixMonet -> PreferKey.useMiuixMonet to update.value
-                ThemeBooleanSetting.EnableBlur -> PreferKey.enableBlur to update.value
-                ThemeBooleanSetting.EnableProgressiveBlur -> PreferKey.enableProgressiveBlur to update.value
-                ThemeBooleanSetting.UseFlexibleTopAppBar -> PreferKey.useFlexibleTopAppBar to update.value
-                ThemeBooleanSetting.BookInfoFollowCoverColor -> PreferKey.bookInfoFollowCoverColor to update.value
-                ThemeBooleanSetting.EnableItemDivider -> PreferKey.enableItemDivider to update.value
-                ThemeBooleanSetting.OverrideBaseCardCornerRadius ->
-                    PreferKey.overrideBaseCardCornerRadius to update.value
-
-                ThemeBooleanSetting.OverrideBaseCardBorder ->
-                    PreferKey.overrideBaseCardBorder to update.value
-
-                ThemeBooleanSetting.DisableSplicedColumnGroupCornerRadius ->
-                    PreferKey.disableSplicedColumnGroupCornerRadius to update.value
-                ThemeBooleanSetting.EyeProtectionEnabled -> PreferKey.eyeProtectionEnabled to update.value
-                ThemeBooleanSetting.EyeProtectionSchedule -> PreferKey.eyeProtectionSchedule to update.value
-                ThemeBooleanSetting.ShowRefactorTip ->
-                    io.legado.app.data.local.preferences.LocalPreferencesKeys.SHOW_THEME_REFACTOR_TIP.name to update.value
-                ThemeBooleanSetting.EnableCustomTagColors -> PreferKey.enableCustomTagColors to update.value
-            }
-            is ThemeSettingsUpdate.IntValue -> when (update.setting) {
-                ThemeIntSetting.ContainerOpacity -> PreferKey.containerOpacity to update.value
-                ThemeIntSetting.TopBarOpacity -> PreferKey.topBarOpacity to update.value
-                ThemeIntSetting.BottomBarOpacity -> PreferKey.bottomBarOpacity to update.value
-                ThemeIntSetting.TopBarBlurRadius -> PreferKey.topBarBlurRadius to update.value
-                ThemeIntSetting.BottomBarBlurRadius -> PreferKey.bottomBarBlurRadius to update.value
-                ThemeIntSetting.TopBarBlurAlpha -> PreferKey.topBarBlurAlpha to update.value
-                ThemeIntSetting.BottomBarBlurAlpha -> PreferKey.bottomBarBlurAlpha to update.value
-                ThemeIntSetting.BackgroundImageBlurring -> PreferKey.bgImageBlurring to update.value
-                ThemeIntSetting.BackgroundImageDarkBlurring -> PreferKey.bgImageNBlurring to update.value
-                ThemeIntSetting.ItemDividerColor -> PreferKey.itemDividerColor to update.value
-                ThemeIntSetting.BaseCardBorderColor -> PreferKey.baseCardBorderColor to update.value
-                ThemeIntSetting.BaseCardBorderColorNight ->
-                    PreferKey.baseCardBorderColorNight to update.value
-                ThemeIntSetting.ColorTemperature -> PreferKey.colorTemperature to update.value
-            }
-            is ThemeSettingsUpdate.FloatValue -> when (update.setting) {
-                ThemeFloatSetting.BottomBarLensRadius -> PreferKey.bottomBarLensRadius to update.value
-                ThemeFloatSetting.ItemDividerWidth -> PreferKey.itemDividerWidth to update.value
-                ThemeFloatSetting.ItemDividerLength -> PreferKey.itemDividerLength to update.value
-                ThemeFloatSetting.BaseCardCornerRadius -> PreferKey.baseCardCornerRadius to update.value
-                ThemeFloatSetting.BaseCardBorderWidth -> PreferKey.baseCardBorderWidth to update.value
-            }
-            is ThemeSettingsUpdate.StringValue -> when (update.setting) {
-                ThemeStringSetting.BookInfoNetworkCoverBackground ->
-                    PreferKey.bookInfoNetworkCoverBackground to update.value
-                ThemeStringSetting.BookInfoDefaultCoverBackground ->
-                    PreferKey.bookInfoDefaultCoverBackground to update.value
-                ThemeStringSetting.BackgroundImageLight -> PreferKey.bgImage to update.value
-                ThemeStringSetting.BackgroundImageDark -> PreferKey.bgImageN to update.value
-                ThemeStringSetting.EyeProtectionStartTime -> PreferKey.eyeProtectionStartTime to update.value
-                ThemeStringSetting.EyeProtectionEndTime -> PreferKey.eyeProtectionEndTime to update.value
-                ThemeStringSetting.CustomTagColorsJson -> PreferKey.customTagColors to update.value
-            }
-            }
-            key to value
-        }
-        AppConfigStore.putAll(values)
+    override suspend fun update(transform: (ThemeSettings) -> ThemeSettings) {
+        AppConfigStore.atomicUpdate(
+            read = Preferences::toThemeSettings,
+            toPrefMap = ThemeSettings::toGatewayPrefMap,
+            transform = transform,
+        )
     }
 }
 
@@ -489,6 +401,73 @@ internal fun Preferences.toThemeSettings(): ThemeSettings = ThemeSettings(
     customTagColorsJson = compatDsString(PreferKey.customTagColors),
 )
 
+/**
+ * Theme gateway 的 59 键写入边界。主题包专用的 [ThemeSettings.customMode] 与
+ * [ThemeSettings.bookInfoInputColor] 由 ThemePackageSettingsGateway 的事务路径持久化。
+ */
+internal fun ThemeSettings.toGatewayPrefMap(): Map<String, Any?> = mapOf(
+    PreferKey.appTheme to appTheme,
+    PreferKey.useMiuixMonet to useMiuixMonet,
+    PreferKey.pureBlack to isPureBlack,
+    PreferKey.paletteStyle to paletteStyle,
+    PreferKey.materialVersion to materialVersion,
+    PreferKey.customContrast to customContrast,
+    PreferKey.appFontPath to appFontPath,
+    PreferKey.cPrimary to customPrimary,
+    PreferKey.cNPrimary to customNightPrimary,
+    PreferKey.enableDeepPersonalization to enableDeepPersonalization,
+    PreferKey.themeColor to themeColor,
+    PreferKey.secondaryThemeColor to secondaryThemeColor,
+    PreferKey.primaryTextColor to primaryTextColor,
+    PreferKey.secondaryTextColor to secondaryTextColor,
+    PreferKey.themeBackgroundColor to themeBackgroundColor,
+    PreferKey.labelContainerColor to labelContainerColor,
+    PreferKey.themeColorNight to themeColorNight,
+    PreferKey.secondaryThemeColorNight to secondaryThemeColorNight,
+    PreferKey.primaryTextColorNight to primaryTextColorNight,
+    PreferKey.secondaryTextColorNight to secondaryTextColorNight,
+    PreferKey.themeBackgroundColorNight to themeBackgroundColorNight,
+    PreferKey.labelContainerColorNight to labelContainerColorNight,
+    PreferKey.containerOpacity to containerOpacity,
+    PreferKey.overrideBaseCardCornerRadius to overrideBaseCardCornerRadius,
+    PreferKey.baseCardCornerRadius to baseCardCornerRadius,
+    PreferKey.overrideBaseCardBorder to overrideBaseCardBorder,
+    PreferKey.baseCardBorderWidth to baseCardBorderWidth,
+    PreferKey.baseCardBorderColor to baseCardBorderColor,
+    PreferKey.baseCardBorderColorNight to baseCardBorderColorNight,
+    PreferKey.disableSplicedColumnGroupCornerRadius to disableSplicedColumnGroupCornerRadius,
+    PreferKey.topBarOpacity to topBarOpacity,
+    PreferKey.bottomBarOpacity to bottomBarOpacity,
+    PreferKey.enableBlur to enableBlur,
+    PreferKey.enableProgressiveBlur to enableProgressiveBlur,
+    PreferKey.topBarBlurRadius to topBarBlurRadius,
+    PreferKey.bottomBarBlurRadius to bottomBarBlurRadius,
+    PreferKey.topBarBlurAlpha to topBarBlurAlpha,
+    PreferKey.bottomBarBlurAlpha to bottomBarBlurAlpha,
+    PreferKey.bottomBarLensRadius to bottomBarLensRadius,
+    PreferKey.useFlexibleTopAppBar to useFlexibleTopAppBar,
+    PreferKey.bookInfoFollowCoverColor to bookInfoFollowCoverColor,
+    PreferKey.bookInfoNetworkCoverBackground to bookInfoNetworkCoverBackground,
+    PreferKey.bookInfoDefaultCoverBackground to bookInfoDefaultCoverBackground,
+    PreferKey.bgImage to backgroundImageLight,
+    PreferKey.bgImageN to backgroundImageDark,
+    PreferKey.bgImageBlurring to backgroundImageBlurring,
+    PreferKey.bgImageNBlurring to backgroundImageDarkBlurring,
+    PreferKey.enableItemDivider to enableItemDivider,
+    PreferKey.itemDividerWidth to itemDividerWidth,
+    PreferKey.itemDividerLength to itemDividerLength,
+    PreferKey.itemDividerColor to itemDividerColor,
+    PreferKey.eyeProtectionEnabled to eyeProtectionEnabled,
+    PreferKey.colorTemperature to colorTemperature,
+    PreferKey.eyeProtectionSchedule to eyeProtectionSchedule,
+    PreferKey.eyeProtectionStartTime to eyeProtectionStartTime,
+    PreferKey.eyeProtectionEndTime to eyeProtectionEndTime,
+    io.legado.app.data.local.preferences.LocalPreferencesKeys.SHOW_THEME_REFACTOR_TIP.name to
+        showRefactorTip,
+    PreferKey.enableCustomTagColors to enableCustomTagColors,
+    PreferKey.customTagColors to customTagColorsJson,
+)
+
 internal fun Preferences.toOtherSettings(): OtherSettings {
     val rawSourceEditMaxLine = compatDsInt(PreferKey.sourceEditMaxLine) ?: Int.MAX_VALUE
     return OtherSettings(

+ 6 - 95
app/src/main/java/io/legado/app/domain/gateway/ThemeSettingsGateway.kt

@@ -6,100 +6,11 @@ import kotlinx.coroutines.flow.Flow
 interface ThemeSettingsGateway {
     val currentSettings: ThemeSettings
     val settings: Flow<ThemeSettings>
-    suspend fun update(update: ThemeSettingsUpdate)
-    suspend fun updateAll(updates: List<ThemeSettingsUpdate>)
-}
-
-sealed interface ThemeSettingsUpdate {
-    data class AppTheme(val value: String) : ThemeSettingsUpdate
-    data class PureBlack(val value: Boolean) : ThemeSettingsUpdate
-    data class PaletteStyle(val value: String) : ThemeSettingsUpdate
-    data class MaterialVersion(val value: String) : ThemeSettingsUpdate
-    data class CustomContrast(val value: String) : ThemeSettingsUpdate
-    data class DeepPersonalization(val value: Boolean) : ThemeSettingsUpdate
-    data class CustomColor(val slot: ThemeColorSlot, val value: Int) : ThemeSettingsUpdate
-    data class AppFontPath(val value: String?) : ThemeSettingsUpdate
-    data class CustomPrimary(val value: Int) : ThemeSettingsUpdate
-    data class CustomNightPrimary(val value: Int) : ThemeSettingsUpdate
-    data class BooleanValue(
-        val setting: ThemeBooleanSetting,
-        val value: Boolean,
-    ) : ThemeSettingsUpdate
-    data class IntValue(
-        val setting: ThemeIntSetting,
-        val value: Int,
-    ) : ThemeSettingsUpdate
-    data class FloatValue(
-        val setting: ThemeFloatSetting,
-        val value: Float,
-    ) : ThemeSettingsUpdate
-    data class StringValue(
-        val setting: ThemeStringSetting,
-        val value: String?,
-    ) : ThemeSettingsUpdate
-}
-
-enum class ThemeBooleanSetting {
-    UseMiuixMonet,
-    EnableBlur,
-    EnableProgressiveBlur,
-    UseFlexibleTopAppBar,
-    BookInfoFollowCoverColor,
-    EnableItemDivider,
-    OverrideBaseCardCornerRadius,
-    OverrideBaseCardBorder,
-    DisableSplicedColumnGroupCornerRadius,
-    EyeProtectionEnabled,
-    EyeProtectionSchedule,
-    ShowRefactorTip,
-    EnableCustomTagColors,
-}
-
-enum class ThemeIntSetting {
-    ContainerOpacity,
-    TopBarOpacity,
-    BottomBarOpacity,
-    TopBarBlurRadius,
-    BottomBarBlurRadius,
-    TopBarBlurAlpha,
-    BottomBarBlurAlpha,
-    BackgroundImageBlurring,
-    BackgroundImageDarkBlurring,
-    ItemDividerColor,
-    BaseCardBorderColor,
-    BaseCardBorderColorNight,
-    ColorTemperature,
-}
-
-enum class ThemeFloatSetting {
-    BottomBarLensRadius,
-    ItemDividerWidth,
-    ItemDividerLength,
-    BaseCardCornerRadius,
-    BaseCardBorderWidth,
-}
-
-enum class ThemeStringSetting {
-    BookInfoNetworkCoverBackground,
-    BookInfoDefaultCoverBackground,
-    BackgroundImageLight,
-    BackgroundImageDark,
-    EyeProtectionStartTime,
-    EyeProtectionEndTime,
-    CustomTagColorsJson,
-}
 
-enum class ThemeColorSlot {
-    Primary,
-    Secondary,
-    PrimaryText,
-    SecondaryText,
-    Background,
-    LabelContainer,
-    PrimaryNight,
-    SecondaryNight,
-    PrimaryTextNight,
-    SecondaryTextNight,
-    BackgroundNight,
-    LabelContainerNight,
+    /**
+     * 仅持久化 gateway 映射声明的 59 个配置键。
+     * [ThemeSettings] 是包含主题包专用字段的 61 字段读模型;[ThemeSettings.customMode] 与
+     * [ThemeSettings.bookInfoInputColor] 仍须由主题包事务写入,不能在此处 copy 后期待自动落盘。
+     */
+    suspend fun update(transform: (ThemeSettings) -> ThemeSettings)
 }

+ 9 - 13
app/src/main/java/io/legado/app/lib/prefs/ThemeCardPreference.kt

@@ -18,9 +18,7 @@ import androidx.lifecycle.lifecycleScope
 import com.google.android.material.card.MaterialCardView
 import io.legado.app.R
 import io.legado.app.constant.PreferKey
-import io.legado.app.domain.gateway.ThemeIntSetting
 import io.legado.app.domain.gateway.ThemeSettingsGateway
-import io.legado.app.domain.gateway.ThemeSettingsUpdate
 import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.utils.getPrefString
 import io.legado.app.utils.toastOnUi
@@ -105,18 +103,16 @@ class ThemeCardPreference(context: Context, attrs: AttributeSet) : Preference(co
                     currentValue = value
                     callChangeListener(value)
                     holder.itemView.activity?.lifecycleScope?.launch {
-                        val updates = buildList {
-                            add(ThemeSettingsUpdate.AppTheme(value))
-                            if (value == "13") {
-                                add(
-                                    ThemeSettingsUpdate.IntValue(
-                                        ThemeIntSetting.ContainerOpacity,
-                                        0,
-                                    )
-                                )
-                            }
+                        themeSettingsGateway.update {
+                            it.copy(
+                                appTheme = value,
+                                containerOpacity = if (value == "13") {
+                                    0
+                                } else {
+                                    it.containerOpacity
+                                },
+                            )
                         }
-                        themeSettingsGateway.updateAll(updates)
                     }
                     notifyDataSetChanged()
                 }

+ 16 - 2
app/src/main/java/io/legado/app/ui/config/customTheme/CustomThemeContract.kt

@@ -1,7 +1,6 @@
 package io.legado.app.ui.config.customTheme
 
 import androidx.compose.runtime.Stable
-import io.legado.app.domain.gateway.ThemeColorSlot
 
 @Stable
 data class CustomThemeUiState(
@@ -27,11 +26,26 @@ data class CustomThemeUiState(
 )
 
 sealed interface CustomThemePicker {
-    data class DeepColor(val slot: ThemeColorSlot) : CustomThemePicker
+    data class DeepColor(val slot: CustomThemeColorSlot) : CustomThemePicker
     data object DaySeed : CustomThemePicker
     data object NightSeed : CustomThemePicker
 }
 
+enum class CustomThemeColorSlot {
+    Primary,
+    Secondary,
+    PrimaryText,
+    SecondaryText,
+    Background,
+    LabelContainer,
+    PrimaryNight,
+    SecondaryNight,
+    PrimaryTextNight,
+    SecondaryTextNight,
+    BackgroundNight,
+    LabelContainerNight,
+}
+
 sealed interface CustomThemeIntent {
     data class DeepPersonalizationChanged(val value: Boolean) : CustomThemeIntent
     data class PaletteStyleChanged(val value: String) : CustomThemeIntent

+ 20 - 21
app/src/main/java/io/legado/app/ui/config/customTheme/CustomThemeScreen.kt

@@ -18,7 +18,6 @@ import androidx.compose.ui.res.stringArrayResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
-import io.legado.app.domain.gateway.ThemeColorSlot
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.SplicedColumnGroup
@@ -173,43 +172,43 @@ private fun CustomColorSettings(
     background: Int,
     labelContainer: Int,
     keySuffix: String,
-    onSelect: (ThemeColorSlot) -> Unit,
+    onSelect: (CustomThemeColorSlot) -> Unit,
 ) {
     SplicedColumnGroup(title = title) {
         CustomColorSettingItem(
             title = stringResource(R.string.theme_manage_primary_color),
             colorValue = primary,
-            onClick = { onSelect(if (keySuffix.isEmpty()) ThemeColorSlot.Primary else ThemeColorSlot.PrimaryNight) },
+            onClick = { onSelect(if (keySuffix.isEmpty()) CustomThemeColorSlot.Primary else CustomThemeColorSlot.PrimaryNight) },
         )
         CustomColorSettingItem(
             title = stringResource(R.string.theme_manage_secondary_color),
             colorValue = secondary,
             onClick = {
                 onSelect(
-                    if (keySuffix.isEmpty()) ThemeColorSlot.Secondary
-                    else ThemeColorSlot.SecondaryNight
+                    if (keySuffix.isEmpty()) CustomThemeColorSlot.Secondary
+                    else CustomThemeColorSlot.SecondaryNight
                 )
             },
         )
         CustomColorSettingItem(
             title = stringResource(R.string.theme_manage_primary_text_color),
             colorValue = primaryText,
-            onClick = { onSelect(if (keySuffix.isEmpty()) ThemeColorSlot.PrimaryText else ThemeColorSlot.PrimaryTextNight) },
+            onClick = { onSelect(if (keySuffix.isEmpty()) CustomThemeColorSlot.PrimaryText else CustomThemeColorSlot.PrimaryTextNight) },
         )
         CustomColorSettingItem(
             title = stringResource(R.string.theme_manage_secondary_text_color),
             colorValue = secondaryText,
-            onClick = { onSelect(if (keySuffix.isEmpty()) ThemeColorSlot.SecondaryText else ThemeColorSlot.SecondaryTextNight) },
+            onClick = { onSelect(if (keySuffix.isEmpty()) CustomThemeColorSlot.SecondaryText else CustomThemeColorSlot.SecondaryTextNight) },
         )
         CustomColorSettingItem(
             title = stringResource(R.string.theme_manage_background_color),
             colorValue = background,
-            onClick = { onSelect(if (keySuffix.isEmpty()) ThemeColorSlot.Background else ThemeColorSlot.BackgroundNight) },
+            onClick = { onSelect(if (keySuffix.isEmpty()) CustomThemeColorSlot.Background else CustomThemeColorSlot.BackgroundNight) },
         )
         CustomColorSettingItem(
             title = stringResource(R.string.theme_manage_label_container_color),
             colorValue = labelContainer,
-            onClick = { onSelect(if (keySuffix.isEmpty()) ThemeColorSlot.LabelContainer else ThemeColorSlot.LabelContainerNight) },
+            onClick = { onSelect(if (keySuffix.isEmpty()) CustomThemeColorSlot.LabelContainer else CustomThemeColorSlot.LabelContainerNight) },
         )
     }
 }
@@ -251,18 +250,18 @@ private fun ColorSwatch(colorValue: Int) {
 
 private fun CustomThemeUiState.colorForPicker(): Int = when (val picker = activePicker) {
     is CustomThemePicker.DeepColor -> when (picker.slot) {
-        ThemeColorSlot.Primary -> themeColor
-        ThemeColorSlot.Secondary -> secondaryThemeColor
-        ThemeColorSlot.PrimaryText -> primaryTextColor
-        ThemeColorSlot.SecondaryText -> secondaryTextColor
-        ThemeColorSlot.Background -> themeBackgroundColor
-        ThemeColorSlot.LabelContainer -> labelContainerColor
-        ThemeColorSlot.PrimaryNight -> themeColorNight
-        ThemeColorSlot.SecondaryNight -> secondaryThemeColorNight
-        ThemeColorSlot.PrimaryTextNight -> primaryTextColorNight
-        ThemeColorSlot.SecondaryTextNight -> secondaryTextColorNight
-        ThemeColorSlot.BackgroundNight -> themeBackgroundColorNight
-        ThemeColorSlot.LabelContainerNight -> labelContainerColorNight
+        CustomThemeColorSlot.Primary -> themeColor
+        CustomThemeColorSlot.Secondary -> secondaryThemeColor
+        CustomThemeColorSlot.PrimaryText -> primaryTextColor
+        CustomThemeColorSlot.SecondaryText -> secondaryTextColor
+        CustomThemeColorSlot.Background -> themeBackgroundColor
+        CustomThemeColorSlot.LabelContainer -> labelContainerColor
+        CustomThemeColorSlot.PrimaryNight -> themeColorNight
+        CustomThemeColorSlot.SecondaryNight -> secondaryThemeColorNight
+        CustomThemeColorSlot.PrimaryTextNight -> primaryTextColorNight
+        CustomThemeColorSlot.SecondaryTextNight -> secondaryTextColorNight
+        CustomThemeColorSlot.BackgroundNight -> themeBackgroundColorNight
+        CustomThemeColorSlot.LabelContainerNight -> labelContainerColorNight
     }
     CustomThemePicker.DaySeed -> primarySeedColor
     CustomThemePicker.NightSeed -> nightPrimarySeedColor

+ 31 - 11
app/src/main/java/io/legado/app/ui/config/customTheme/CustomThemeViewModel.kt

@@ -3,7 +3,6 @@ package io.legado.app.ui.config.customTheme
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import io.legado.app.domain.gateway.ThemeSettingsGateway
-import io.legado.app.domain.gateway.ThemeSettingsUpdate
 import io.legado.app.domain.model.settings.ThemeSettings
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
@@ -32,13 +31,13 @@ class CustomThemeViewModel(
     fun onIntent(intent: CustomThemeIntent) {
         when (intent) {
             is CustomThemeIntent.DeepPersonalizationChanged ->
-                update(ThemeSettingsUpdate.DeepPersonalization(intent.value))
+                update { it.copy(enableDeepPersonalization = intent.value) }
             is CustomThemeIntent.PaletteStyleChanged ->
-                update(ThemeSettingsUpdate.PaletteStyle(intent.value))
+                update { it.copy(paletteStyle = intent.value) }
             is CustomThemeIntent.CustomContrastChanged ->
-                update(ThemeSettingsUpdate.CustomContrast(intent.value))
+                update { it.copy(customContrast = intent.value) }
             is CustomThemeIntent.MaterialVersionChanged ->
-                update(ThemeSettingsUpdate.MaterialVersion(intent.value))
+                update { it.copy(materialVersion = intent.value) }
             is CustomThemeIntent.OpenPicker ->
                 _uiState.update { it.copy(activePicker = intent.picker) }
             CustomThemeIntent.DismissPicker ->
@@ -49,21 +48,42 @@ class CustomThemeViewModel(
 
     private fun selectColor(color: Int) {
         when (val picker = _uiState.value.activePicker) {
-            is CustomThemePicker.DeepColor ->
-                update(ThemeSettingsUpdate.CustomColor(picker.slot, color))
+            is CustomThemePicker.DeepColor -> update { settings ->
+                when (picker.slot) {
+                    CustomThemeColorSlot.Primary -> settings.copy(themeColor = color)
+                    CustomThemeColorSlot.Secondary -> settings.copy(secondaryThemeColor = color)
+                    CustomThemeColorSlot.PrimaryText -> settings.copy(primaryTextColor = color)
+                    CustomThemeColorSlot.SecondaryText ->
+                        settings.copy(secondaryTextColor = color)
+                    CustomThemeColorSlot.Background -> settings.copy(themeBackgroundColor = color)
+                    CustomThemeColorSlot.LabelContainer ->
+                        settings.copy(labelContainerColor = color)
+                    CustomThemeColorSlot.PrimaryNight -> settings.copy(themeColorNight = color)
+                    CustomThemeColorSlot.SecondaryNight ->
+                        settings.copy(secondaryThemeColorNight = color)
+                    CustomThemeColorSlot.PrimaryTextNight ->
+                        settings.copy(primaryTextColorNight = color)
+                    CustomThemeColorSlot.SecondaryTextNight ->
+                        settings.copy(secondaryTextColorNight = color)
+                    CustomThemeColorSlot.BackgroundNight ->
+                        settings.copy(themeBackgroundColorNight = color)
+                    CustomThemeColorSlot.LabelContainerNight ->
+                        settings.copy(labelContainerColorNight = color)
+                }
+            }
             CustomThemePicker.DaySeed -> {
-                update(ThemeSettingsUpdate.CustomPrimary(color))
+                update { it.copy(customPrimary = color) }
                 _effects.tryEmit(CustomThemeEffect.ApplyLegacyPrimarySeed(color))
             }
-            CustomThemePicker.NightSeed -> update(ThemeSettingsUpdate.CustomNightPrimary(color))
+            CustomThemePicker.NightSeed -> update { it.copy(customNightPrimary = color) }
             null -> return
         }
         _uiState.update { it.copy(activePicker = null) }
     }
 
-    private fun update(update: ThemeSettingsUpdate) {
+    private fun update(transform: (ThemeSettings) -> ThemeSettings) {
         viewModelScope.launch {
-            runCatching { themeSettingsGateway.update(update) }
+            runCatching { themeSettingsGateway.update(transform) }
                 .onFailure { error ->
                     _effects.tryEmit(
                         CustomThemeEffect.SettingsUpdateFailed(

+ 3 - 2
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigContract.kt

@@ -1,7 +1,6 @@
 package io.legado.app.ui.config.themeConfig
 
 import androidx.compose.runtime.Stable
-import io.legado.app.domain.gateway.ThemeSettingsUpdate
 import io.legado.app.domain.model.settings.AppShellSettings
 import io.legado.app.domain.model.settings.ThemeSettings
 import io.legado.app.utils.FileDoc
@@ -31,7 +30,9 @@ enum class ThemeTimeField {
 }
 
 sealed interface ThemeConfigIntent {
-    data class UpdateTheme(val update: ThemeSettingsUpdate) : ThemeConfigIntent
+    data class UpdateTheme(
+        val transform: (ThemeSettings) -> ThemeSettings,
+    ) : ThemeConfigIntent
     data class ShowSheet(val sheet: ThemeConfigSheet) : ThemeConfigIntent
     data object DismissSheet : ThemeConfigIntent
     data class SelectTheme(val value: String) : ThemeConfigIntent

+ 67 - 133
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt

@@ -60,11 +60,7 @@ import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.constraintlayout.compose.ConstraintLayout
 import io.legado.app.R
-import io.legado.app.domain.gateway.ThemeBooleanSetting
-import io.legado.app.domain.gateway.ThemeFloatSetting
-import io.legado.app.domain.gateway.ThemeIntSetting
-import io.legado.app.domain.gateway.ThemeSettingsUpdate
-import io.legado.app.domain.gateway.ThemeStringSetting
+import io.legado.app.domain.model.settings.ThemeSettings
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.ThemeEngine
 import io.legado.app.ui.theme.ThemeResolver
@@ -99,15 +95,8 @@ fun ThemeConfigScreen(
     val theme = state.theme
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
     val context = LocalContext.current
-    fun updateTheme(setting: ThemeBooleanSetting, value: Boolean) = onIntent(
-        ThemeConfigIntent.UpdateTheme(ThemeSettingsUpdate.BooleanValue(setting, value))
-    )
-    fun updateTheme(setting: ThemeIntSetting, value: Int) = onIntent(
-        ThemeConfigIntent.UpdateTheme(ThemeSettingsUpdate.IntValue(setting, value))
-    )
-    fun updateTheme(setting: ThemeFloatSetting, value: Float) = onIntent(
-        ThemeConfigIntent.UpdateTheme(ThemeSettingsUpdate.FloatValue(setting, value))
-    )
+    fun updateTheme(transform: (ThemeSettings) -> ThemeSettings) =
+        onIntent(ThemeConfigIntent.UpdateTheme(transform))
     val fontFolderState = remember(state.fontFolder) {
         FontFolderState.Loaded(state.fontFolder.takeIf { it.isNotEmpty() }?.let(android.net.Uri::parse))
     }
@@ -383,15 +372,8 @@ fun ThemeConfigScreen(
                                 value = theme.bottomBarLensRadius,
                                 defaultValue = 24f,
                                 valueRange = 0f..50f,
-                                onValueChange = {
-                                    onIntent(
-                                        ThemeConfigIntent.UpdateTheme(
-                                            ThemeSettingsUpdate.FloatValue(
-                                                ThemeFloatSetting.BottomBarLensRadius,
-                                                it,
-                                            )
-                                        )
-                                    )
+                                onValueChange = { value ->
+                                    updateTheme { it.copy(bottomBarLensRadius = value) }
                                 }
                             )
                         }
@@ -426,15 +408,8 @@ fun ThemeConfigScreen(
                         title = stringResource(R.string.book_info_follow_cover_color),
                         description = stringResource(R.string.book_info_follow_cover_color_summary),
                         checked = theme.bookInfoFollowCoverColor,
-                        onCheckedChange = {
-                            onIntent(
-                                ThemeConfigIntent.UpdateTheme(
-                                    ThemeSettingsUpdate.BooleanValue(
-                                        ThemeBooleanSetting.BookInfoFollowCoverColor,
-                                        it,
-                                    )
-                                )
-                            )
+                        onCheckedChange = { value ->
+                            updateTheme { it.copy(bookInfoFollowCoverColor = value) }
                         }
                     )
                     DropdownListSettingItem(
@@ -442,15 +417,8 @@ fun ThemeConfigScreen(
                         selectedValue = theme.bookInfoNetworkCoverBackground,
                         displayEntries = stringArrayResource(R.array.book_info_background_blur_entries),
                         entryValues = stringArrayResource(R.array.book_info_background_blur_values),
-                        onValueChange = {
-                            onIntent(
-                                ThemeConfigIntent.UpdateTheme(
-                                    ThemeSettingsUpdate.StringValue(
-                                        ThemeStringSetting.BookInfoNetworkCoverBackground,
-                                        it,
-                                    )
-                                )
-                            )
+                        onValueChange = { value ->
+                            updateTheme { it.copy(bookInfoNetworkCoverBackground = value) }
                         }
                     )
                     DropdownListSettingItem(
@@ -458,15 +426,8 @@ fun ThemeConfigScreen(
                         selectedValue = theme.bookInfoDefaultCoverBackground,
                         displayEntries = stringArrayResource(R.array.book_info_background_blur_entries),
                         entryValues = stringArrayResource(R.array.book_info_background_blur_values),
-                        onValueChange = {
-                            onIntent(
-                                ThemeConfigIntent.UpdateTheme(
-                                    ThemeSettingsUpdate.StringValue(
-                                        ThemeStringSetting.BookInfoDefaultCoverBackground,
-                                        it,
-                                    )
-                                )
-                            )
+                        onValueChange = { value ->
+                            updateTheme { it.copy(bookInfoDefaultCoverBackground = value) }
                         }
                     )
                 }
@@ -476,15 +437,8 @@ fun ThemeConfigScreen(
                         title = stringResource(R.string.eye_protection_enabled),
                         description = stringResource(R.string.eye_protection_enabled_summary),
                         checked = theme.eyeProtectionEnabled,
-                        onCheckedChange = {
-                            onIntent(
-                                ThemeConfigIntent.UpdateTheme(
-                                    ThemeSettingsUpdate.BooleanValue(
-                                        ThemeBooleanSetting.EyeProtectionEnabled,
-                                        it,
-                                    )
-                                )
-                            )
+                        onCheckedChange = { value ->
+                            updateTheme { it.copy(eyeProtectionEnabled = value) }
                         }
                     )
 
@@ -499,15 +453,8 @@ fun ThemeConfigScreen(
                                 value = theme.colorTemperature.toFloat(),
                                 defaultValue = 50f,
                                 valueRange = 0f..100f,
-                                onValueChange = {
-                                    onIntent(
-                                        ThemeConfigIntent.UpdateTheme(
-                                            ThemeSettingsUpdate.IntValue(
-                                                ThemeIntSetting.ColorTemperature,
-                                                it.toInt(),
-                                            )
-                                        )
-                                    )
+                                onValueChange = { value ->
+                                    updateTheme { it.copy(colorTemperature = value.toInt()) }
                                 }
                             )
 
@@ -515,15 +462,8 @@ fun ThemeConfigScreen(
                                 title = stringResource(R.string.eye_protection_schedule),
                                 description = stringResource(R.string.eye_protection_schedule_summary),
                                 checked = theme.eyeProtectionSchedule,
-                                onCheckedChange = {
-                                    onIntent(
-                                        ThemeConfigIntent.UpdateTheme(
-                                            ThemeSettingsUpdate.BooleanValue(
-                                                ThemeBooleanSetting.EyeProtectionSchedule,
-                                                it,
-                                            )
-                                        )
-                                    )
+                                onCheckedChange = { value ->
+                                    updateTheme { it.copy(eyeProtectionSchedule = value) }
                                 }
                             )
 
@@ -564,19 +504,15 @@ fun ThemeConfigScreen(
                         SwitchSettingItem(
                             title = stringResource(R.string.pure_black),
                             checked = theme.isPureBlack,
-                            onCheckedChange = {
-                                onIntent(
-                                    ThemeConfigIntent.UpdateTheme(
-                                        ThemeSettingsUpdate.PureBlack(it)
-                                    )
-                                )
+                            onCheckedChange = { value ->
+                                updateTheme { it.copy(isPureBlack = value) }
                             }
                         )
                         SwitchSettingItem(
                             title = stringResource(R.string.use_flexible_top_bar),
                             checked = theme.useFlexibleTopAppBar,
-                            onCheckedChange = {
-                                updateTheme(ThemeBooleanSetting.UseFlexibleTopAppBar, it)
+                            onCheckedChange = { value ->
+                                updateTheme { it.copy(useFlexibleTopAppBar = value) }
                             }
                         )
                     }
@@ -589,8 +525,8 @@ fun ThemeConfigScreen(
                         SwitchSettingItem(
                             title = stringResource(R.string.is_blur_progressive_enable),
                             checked = theme.enableProgressiveBlur,
-                            onCheckedChange = {
-                                updateTheme(ThemeBooleanSetting.EnableProgressiveBlur, it)
+                            onCheckedChange = { value ->
+                                updateTheme { it.copy(enableProgressiveBlur = value) }
                             }
                         )
                     }
@@ -601,8 +537,8 @@ fun ThemeConfigScreen(
                             value = theme.topBarBlurRadius.toFloat(),
                             defaultValue = 24f,
                             valueRange = 0f..30f,
-                            onValueChange = {
-                                updateTheme(ThemeIntSetting.TopBarBlurRadius, it.toInt())
+                            onValueChange = { value ->
+                                updateTheme { it.copy(topBarBlurRadius = value.toInt()) }
                             }
                         )
                         SliderSettingItem(
@@ -611,8 +547,8 @@ fun ThemeConfigScreen(
                             value = theme.bottomBarBlurRadius.toFloat(),
                             defaultValue = 8f,
                             valueRange = 0f..10f,
-                            onValueChange = {
-                                updateTheme(ThemeIntSetting.BottomBarBlurRadius, it.toInt())
+                            onValueChange = { value ->
+                                updateTheme { it.copy(bottomBarBlurRadius = value.toInt()) }
                             }
                         )
                         SliderSettingItem(
@@ -620,8 +556,8 @@ fun ThemeConfigScreen(
                             value = theme.topBarBlurAlpha.toFloat(),
                             defaultValue = 73f,
                             valueRange = 0f..100f,
-                            onValueChange = {
-                                updateTheme(ThemeIntSetting.TopBarBlurAlpha, it.toInt())
+                            onValueChange = { value ->
+                                updateTheme { it.copy(topBarBlurAlpha = value.toInt()) }
                             }
                         )
                         SliderSettingItem(
@@ -629,8 +565,8 @@ fun ThemeConfigScreen(
                             value = theme.bottomBarBlurAlpha.toFloat(),
                             defaultValue = 40f,
                             valueRange = 0f..100f,
-                            onValueChange = {
-                                updateTheme(ThemeIntSetting.BottomBarBlurAlpha, it.toInt())
+                            onValueChange = { value ->
+                                updateTheme { it.copy(bottomBarBlurAlpha = value.toInt()) }
                             }
                         )
                     }
@@ -647,8 +583,8 @@ fun ThemeConfigScreen(
                                 value = theme.topBarOpacity.toFloat(),
                                 defaultValue = 100f,
                                 valueRange = 0f..100f,
-                                onValueChange = {
-                                    updateTheme(ThemeIntSetting.TopBarOpacity, it.toInt())
+                                onValueChange = { value ->
+                                    updateTheme { it.copy(topBarOpacity = value.toInt()) }
                                 }
                             )
                             SliderSettingItem(
@@ -660,8 +596,8 @@ fun ThemeConfigScreen(
                                 value = theme.bottomBarOpacity.toFloat(),
                                 defaultValue = 100f,
                                 valueRange = 0f..100f,
-                                onValueChange = {
-                                    updateTheme(ThemeIntSetting.BottomBarOpacity, it.toInt())
+                                onValueChange = { value ->
+                                    updateTheme { it.copy(bottomBarOpacity = value.toInt()) }
                                 }
                             )
                         }
@@ -676,8 +612,8 @@ fun ThemeConfigScreen(
                             value = theme.containerOpacity.toFloat(),
                             defaultValue = 100f,
                             valueRange = 0f..100f,
-                            onValueChange = {
-                                updateTheme(ThemeIntSetting.ContainerOpacity, it.toInt())
+                            onValueChange = { value ->
+                                updateTheme { it.copy(containerOpacity = value.toInt()) }
                             }
                         )
                     }
@@ -703,8 +639,8 @@ fun ThemeConfigScreen(
                             value = theme.backgroundImageBlurring.toFloat(),
                             defaultValue = 0f,
                             valueRange = 0f..100f,
-                            onValueChange = {
-                                updateTheme(ThemeIntSetting.BackgroundImageBlurring, it.toInt())
+                            onValueChange = { value ->
+                                updateTheme { it.copy(backgroundImageBlurring = value.toInt()) }
                             }
                         )
                     }
@@ -727,8 +663,8 @@ fun ThemeConfigScreen(
                             value = theme.backgroundImageDarkBlurring.toFloat(),
                             defaultValue = 0f,
                             valueRange = 0f..100f,
-                            onValueChange = {
-                                updateTheme(ThemeIntSetting.BackgroundImageDarkBlurring, it.toInt())
+                            onValueChange = { value ->
+                                updateTheme { it.copy(backgroundImageDarkBlurring = value.toInt()) }
                             }
                         )
                     }
@@ -742,19 +678,18 @@ fun ThemeConfigScreen(
                         title = stringResource(R.string.disable_spliced_group_corner_radius),
                         description = stringResource(R.string.disable_spliced_group_corner_radius_summary),
                         checked = theme.disableSplicedColumnGroupCornerRadius,
-                        onCheckedChange = {
-                            updateTheme(
-                                ThemeBooleanSetting.DisableSplicedColumnGroupCornerRadius,
-                                it
-                            )
+                        onCheckedChange = { value ->
+                            updateTheme {
+                                it.copy(disableSplicedColumnGroupCornerRadius = value)
+                            }
                         }
                     )
                     SwitchSettingItem(
                         title = stringResource(R.string.base_card_corner_radius_override),
                         description = stringResource(R.string.base_card_override_summary),
                         checked = theme.overrideBaseCardCornerRadius,
-                        onCheckedChange = {
-                            updateTheme(ThemeBooleanSetting.OverrideBaseCardCornerRadius, it)
+                        onCheckedChange = { value ->
+                            updateTheme { it.copy(overrideBaseCardCornerRadius = value) }
                         }
                     )
                     AnimatedVisibility(visible = theme.overrideBaseCardCornerRadius) {
@@ -766,8 +701,8 @@ fun ThemeConfigScreen(
                             valueRange = 0f..40f,
                             steps = 79,
                             decimal = true,
-                            onValueChange = {
-                                updateTheme(ThemeFloatSetting.BaseCardCornerRadius, it)
+                            onValueChange = { value ->
+                                updateTheme { it.copy(baseCardCornerRadius = value) }
                             }
                         )
                     }
@@ -775,8 +710,8 @@ fun ThemeConfigScreen(
                         title = stringResource(R.string.base_card_border_override),
                         description = stringResource(R.string.base_card_override_summary),
                         checked = theme.overrideBaseCardBorder,
-                        onCheckedChange = {
-                            updateTheme(ThemeBooleanSetting.OverrideBaseCardBorder, it)
+                        onCheckedChange = { value ->
+                            updateTheme { it.copy(overrideBaseCardBorder = value) }
                         }
                     )
                     AnimatedVisibility(visible = theme.overrideBaseCardBorder) {
@@ -789,8 +724,8 @@ fun ThemeConfigScreen(
                                 valueRange = 0f..5f,
                                 steps = 49,
                                 decimal = true,
-                                onValueChange = {
-                                    updateTheme(ThemeFloatSetting.BaseCardBorderWidth, it)
+                                onValueChange = { value ->
+                                    updateTheme { it.copy(baseCardBorderWidth = value) }
                                 }
                             )
                             BaseCardBorderColorSettingItem(
@@ -820,8 +755,8 @@ fun ThemeConfigScreen(
                     SwitchSettingItem(
                         title = stringResource(R.string.show_divider_line),
                         checked = theme.enableItemDivider,
-                        onCheckedChange = {
-                            updateTheme(ThemeBooleanSetting.EnableItemDivider, it)
+                        onCheckedChange = { value ->
+                            updateTheme { it.copy(enableItemDivider = value) }
                         }
                     )
                     if (theme.enableItemDivider) {
@@ -833,8 +768,8 @@ fun ThemeConfigScreen(
                             valueRange = 0f..5f,
                             steps = 49,
                             decimal = true,
-                            onValueChange = {
-                                updateTheme(ThemeFloatSetting.ItemDividerWidth, it)
+                            onValueChange = { value ->
+                                updateTheme { it.copy(itemDividerWidth = value) }
                             }
                         )
                         SliderSettingItem(
@@ -844,8 +779,8 @@ fun ThemeConfigScreen(
                             defaultValue = 80f,
                             valueRange = 30f..100f,
                             steps = 14,
-                            onValueChange = {
-                                updateTheme(ThemeFloatSetting.ItemDividerLength, it)
+                            onValueChange = { value ->
+                                updateTheme { it.copy(itemDividerLength = value) }
                             }
                         )
                         ClickableSettingItem(
@@ -945,8 +880,8 @@ fun ThemeConfigScreen(
         show = state.activeSheet == ThemeConfigSheet.DividerColor,
         initialColor = theme.itemDividerColor,
         onDismissRequest = { onIntent(ThemeConfigIntent.DismissSheet) },
-        onColorSelected = {
-            updateTheme(ThemeIntSetting.ItemDividerColor, it)
+        onColorSelected = { value ->
+            updateTheme { it.copy(itemDividerColor = value) }
             onIntent(ThemeConfigIntent.DismissSheet)
         }
     )
@@ -960,15 +895,14 @@ fun ThemeConfigScreen(
             theme.baseCardBorderColor
         },
         onDismissRequest = { onIntent(ThemeConfigIntent.DismissSheet) },
-        onColorSelected = {
-            updateTheme(
+        onColorSelected = { value ->
+            updateTheme {
                 if (baseCardBorderColorSheet?.dark == true) {
-                    ThemeIntSetting.BaseCardBorderColorNight
+                    it.copy(baseCardBorderColorNight = value)
                 } else {
-                    ThemeIntSetting.BaseCardBorderColor
-                },
-                it
-            )
+                    it.copy(baseCardBorderColor = value)
+                }
+            }
             onIntent(ThemeConfigIntent.DismissSheet)
         }
     )

+ 43 - 61
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt

@@ -7,13 +7,10 @@ import io.legado.app.R
 import io.legado.app.constant.PreferKey
 import io.legado.app.domain.gateway.AppShellSettingsGateway
 import io.legado.app.domain.gateway.ReadSettingsGateway
-import io.legado.app.domain.gateway.ThemeBooleanSetting
-import io.legado.app.domain.gateway.ThemeIntSetting
 import io.legado.app.domain.gateway.ThemeSettingsGateway
-import io.legado.app.domain.gateway.ThemeSettingsUpdate
-import io.legado.app.domain.gateway.ThemeStringSetting
 import io.legado.app.domain.gateway.LabSettingsGateway
 import io.legado.app.domain.model.settings.AppShellSettings
+import io.legado.app.domain.model.settings.ThemeSettings
 import io.legado.app.ui.main.MainDestination
 import io.legado.app.utils.FileDoc
 import io.legado.app.utils.FileUtils
@@ -77,7 +74,7 @@ class ThemeConfigViewModel(
 
     fun onIntent(intent: ThemeConfigIntent) {
         when (intent) {
-            is ThemeConfigIntent.UpdateTheme -> updateTheme(intent.update)
+            is ThemeConfigIntent.UpdateTheme -> updateTheme(intent.transform)
             is ThemeConfigIntent.ShowSheet ->
                 _uiState.update { it.copy(activeSheet = intent.sheet) }
             ThemeConfigIntent.DismissSheet ->
@@ -139,7 +136,7 @@ class ThemeConfigViewModel(
             is ThemeConfigIntent.SelectBackground -> setBackground(intent.uri, intent.dark)
             is ThemeConfigIntent.RemoveBackground -> removeBackground(intent.dark)
             is ThemeConfigIntent.SelectAppFont -> setAppFont(intent.file)
-            ThemeConfigIntent.ClearAppFont -> updateTheme(ThemeSettingsUpdate.AppFontPath(null))
+            ThemeConfigIntent.ClearAppFont -> updateTheme { it.copy(appFontPath = null) }
             is ThemeConfigIntent.SetFontFolder -> viewModelScope.launch {
                 readSettingsGateway.update { it.copy(fontFolder = intent.path) }
             }
@@ -148,9 +145,9 @@ class ThemeConfigViewModel(
                 ThemeConfigEffect.OpenTimePicker(intent.field, intent.currentValue)
             )
             is ThemeConfigIntent.SetTime -> setTime(intent.field, intent.value)
-            ThemeConfigIntent.DismissRefactorTip -> updateTheme(
-                ThemeSettingsUpdate.BooleanValue(ThemeBooleanSetting.ShowRefactorTip, false)
-            )
+            ThemeConfigIntent.DismissRefactorTip -> updateTheme {
+                it.copy(showRefactorTip = false)
+            }
         }
     }
 
@@ -164,8 +161,8 @@ class ThemeConfigViewModel(
         }
     }
 
-    private fun updateTheme(update: ThemeSettingsUpdate) {
-        viewModelScope.launch { themeSettingsGateway.update(update) }
+    private fun updateTheme(transform: (ThemeSettings) -> ThemeSettings) {
+        viewModelScope.launch { themeSettingsGateway.update(transform) }
     }
 
     private fun selectTheme(value: String) {
@@ -177,32 +174,27 @@ class ThemeConfigViewModel(
             return
         }
         viewModelScope.launch {
-            val updates = buildList {
-                if (value == "13") {
-                    add(ThemeSettingsUpdate.IntValue(ThemeIntSetting.ContainerOpacity, 0))
-                }
-                add(ThemeSettingsUpdate.AppTheme(value))
+            themeSettingsGateway.update {
+                it.copy(
+                    appTheme = value,
+                    containerOpacity = if (value == "13") 0 else it.containerOpacity,
+                )
             }
-            themeSettingsGateway.updateAll(updates)
         }
     }
 
     private fun setMiuixMonet(enabled: Boolean) {
         viewModelScope.launch {
-            val currentTheme = _uiState.value.theme.appTheme
-            themeSettingsGateway.updateAll(
-                buildList {
-                    add(
-                        ThemeSettingsUpdate.BooleanValue(
-                            ThemeBooleanSetting.UseMiuixMonet,
-                            enabled,
-                        )
-                    )
-                    if (enabled && currentTheme != "0" && currentTheme != "12") {
-                        add(ThemeSettingsUpdate.AppTheme("0"))
-                    }
-                }
-            )
+            themeSettingsGateway.update {
+                it.copy(
+                    useMiuixMonet = enabled,
+                    appTheme = if (enabled && it.appTheme != "0" && it.appTheme != "12") {
+                        "0"
+                    } else {
+                        it.appTheme
+                    },
+                )
+            }
         }
     }
 
@@ -213,24 +205,16 @@ class ThemeConfigViewModel(
 
     private fun setBlurEnabled(enabled: Boolean) {
         viewModelScope.launch {
-            themeSettingsGateway.updateAll(
-                buildList {
-                    add(
-                        ThemeSettingsUpdate.BooleanValue(
-                            ThemeBooleanSetting.EnableBlur,
-                            enabled,
-                        )
-                    )
-                    if (!enabled) {
-                        add(
-                            ThemeSettingsUpdate.BooleanValue(
-                                ThemeBooleanSetting.EnableProgressiveBlur,
-                                false,
-                            )
-                        )
-                    }
-                }
-            )
+            themeSettingsGateway.update {
+                it.copy(
+                    enableBlur = enabled,
+                    enableProgressiveBlur = if (enabled) {
+                        it.enableProgressiveBlur
+                    } else {
+                        false
+                    },
+                )
+            }
         }
     }
 
@@ -297,11 +281,12 @@ class ThemeConfigViewModel(
     }
 
     private fun setTime(field: ThemeTimeField, value: String) {
-        val setting = when (field) {
-            ThemeTimeField.EyeProtectionStart -> ThemeStringSetting.EyeProtectionStartTime
-            ThemeTimeField.EyeProtectionEnd -> ThemeStringSetting.EyeProtectionEndTime
+        updateTheme {
+            when (field) {
+                ThemeTimeField.EyeProtectionStart -> it.copy(eyeProtectionStartTime = value)
+                ThemeTimeField.EyeProtectionEnd -> it.copy(eyeProtectionEndTime = value)
+            }
         }
-        updateTheme(ThemeSettingsUpdate.StringValue(setting, value))
     }
 
     private fun setBackground(uriString: String, dark: Boolean) {
@@ -340,13 +325,10 @@ class ThemeConfigViewModel(
                 .takeIf { it.absolutePath.contains(appCtx.externalFiles.absolutePath) }
                 ?.delete()
         }
-        themeSettingsGateway.update(
-            ThemeSettingsUpdate.StringValue(
-                if (dark) ThemeStringSetting.BackgroundImageDark
-                else ThemeStringSetting.BackgroundImageLight,
-                newPath,
-            )
-        )
+        themeSettingsGateway.update {
+            if (dark) it.copy(backgroundImageDark = newPath)
+            else it.copy(backgroundImageLight = newPath)
+        }
     }
 
     private fun setAppFont(fileDoc: FileDoc) {
@@ -367,7 +349,7 @@ class ThemeConfigViewModel(
                     temp.copyTo(target, overwrite = true)
                     temp.delete()
                 }
-                themeSettingsGateway.update(ThemeSettingsUpdate.AppFontPath(target.absolutePath))
+                themeSettingsGateway.update { it.copy(appFontPath = target.absolutePath) }
             }.onFailure(Throwable::printStackTrace)
         }
     }

+ 2 - 11
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfConfigSheet.kt

@@ -31,8 +31,6 @@ import androidx.compose.ui.res.stringArrayResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
-import io.legado.app.domain.gateway.ThemeBooleanSetting
-import io.legado.app.domain.gateway.ThemeSettingsUpdate
 import io.legado.app.domain.model.settings.BookshelfSettings
 import io.legado.app.ui.config.themeConfig.LabelColorManageSheet
 import io.legado.app.ui.config.themeConfig.TagColorPair
@@ -55,7 +53,7 @@ fun BookshelfConfigSheet(
     enableCustomTagColors: Boolean,
     customTagColors: List<TagColorPair>,
     themeColor: Int,
-    onThemeUpdate: (ThemeSettingsUpdate) -> Unit,
+    onCustomTagColorsEnabledChange: (Boolean) -> Unit,
     onCustomTagColorsChange: (List<TagColorPair>) -> Unit,
     onDismissRequest: () -> Unit
 ) {
@@ -482,14 +480,7 @@ fun BookshelfConfigSheet(
                                         title = stringResource(R.string.custom_tag_colors),
                                         checked = enableCustomTagColors,
                                         color = LegadoTheme.colorScheme.surface,
-                                        onCheckedChange = {
-                                            onThemeUpdate(
-                                                ThemeSettingsUpdate.BooleanValue(
-                                                    ThemeBooleanSetting.EnableCustomTagColors,
-                                                    it,
-                                                )
-                                            )
-                                        }
+                                        onCheckedChange = onCustomTagColorsEnabledChange
                                     )
                                     AnimatedVisibility(visible = enableCustomTagColors) {
                                         CompactClickableSettingItem(

+ 3 - 1
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

@@ -1193,7 +1193,9 @@ private fun BookshelfOverlays(
         enableCustomTagColors = uiState.enableCustomTagColors,
         customTagColors = uiState.customTagColors,
         themeColor = uiState.themeColor,
-        onThemeUpdate = { onIntent(BookshelfIntent.UpdateThemeSetting(it)) },
+        onCustomTagColorsEnabledChange = {
+            onIntent(BookshelfIntent.SetCustomTagColorsEnabled(it))
+        },
         onCustomTagColorsChange = {
             onIntent(BookshelfIntent.SetCustomTagColors(it))
         },

+ 1 - 2
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt

@@ -3,7 +3,6 @@ package io.legado.app.ui.main.bookshelf
 import androidx.compose.runtime.Stable
 import android.net.Uri
 import io.legado.app.data.entities.BookGroup
-import io.legado.app.domain.gateway.ThemeSettingsUpdate
 import io.legado.app.domain.model.settings.BookshelfSettings
 import io.legado.app.ui.config.themeConfig.TagColorPair
 import io.legado.app.ui.widget.components.list.ListUiState
@@ -64,7 +63,7 @@ sealed interface BookshelfIntent {
     data class UpdateSetting(
         val transform: (BookshelfSettings) -> BookshelfSettings,
     ) : BookshelfIntent
-    data class UpdateThemeSetting(val update: ThemeSettingsUpdate) : BookshelfIntent
+    data class SetCustomTagColorsEnabled(val enabled: Boolean) : BookshelfIntent
     data class SetCustomTagColors(val colors: List<TagColorPair>) : BookshelfIntent
     data object UploadResultConsumed : BookshelfIntent
 }

+ 7 - 8
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt

@@ -697,16 +697,15 @@ class BookshelfViewModel(
             is BookshelfIntent.UpdateSetting -> viewModelScope.launch {
                 bookshelfSettingsGateway.update(intent.transform)
             }
-            is BookshelfIntent.UpdateThemeSetting -> viewModelScope.launch {
-                themeSettingsGateway.update(intent.update)
+            is BookshelfIntent.SetCustomTagColorsEnabled -> viewModelScope.launch {
+                themeSettingsGateway.update {
+                    it.copy(enableCustomTagColors = intent.enabled)
+                }
             }
             is BookshelfIntent.SetCustomTagColors -> viewModelScope.launch {
-                themeSettingsGateway.update(
-                    io.legado.app.domain.gateway.ThemeSettingsUpdate.StringValue(
-                        io.legado.app.domain.gateway.ThemeStringSetting.CustomTagColorsJson,
-                        GSON.toJson(intent.colors),
-                    )
-                )
+                themeSettingsGateway.update {
+                    it.copy(customTagColorsJson = GSON.toJson(intent.colors))
+                }
             }
             BookshelfIntent.UploadResultConsumed -> pendingUploadUrlFlow.value = null
         }

+ 276 - 0
app/src/test/java/io/legado/app/data/repository/ThemeSettingsMappingTest.kt

@@ -0,0 +1,276 @@
+package io.legado.app.data.repository
+
+import androidx.datastore.preferences.core.intPreferencesKey
+import androidx.datastore.preferences.core.mutablePreferencesOf
+import androidx.datastore.preferences.core.stringPreferencesKey
+import io.legado.app.constant.PreferKey
+import io.legado.app.data.local.preferences.LocalPreferencesKeys
+import io.legado.app.domain.model.settings.ThemeSettings
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Test
+
+class ThemeSettingsMappingTest {
+
+    @Test
+    fun `Theme gateway 持久化边界固定为 59 键`() {
+        val actualKeys = ThemeSettings().toGatewayPrefMap().keys
+        val expectedKeys = ThemeSettings().expectedGatewayPrefMap().keys
+
+        assertEquals(59, actualKeys.size)
+        assertEquals(expectedKeys, actualKeys)
+        assertFalse(PreferKey.customMode in actualKeys)
+        assertFalse(PreferKey.bookInfoInputColor in actualKeys)
+    }
+
+    @Test
+    fun `Theme gateway 59 键写读映射逐字段对应`() {
+        themeMappingSamples().forEach { expected ->
+            assertEquals(expected.expectedGatewayPrefMap(), expected.toGatewayPrefMap())
+            assertEquals(
+                expected,
+                expected.expectedGatewayPrefMap().toTestPreferences().toThemeSettings(),
+            )
+        }
+    }
+
+    @Test
+    fun `gateway 排除字段仍由 Theme 读模型读取`() {
+        val preferences = mutablePreferencesOf(
+            stringPreferencesKey(PreferKey.customMode) to "accent",
+            intPreferencesKey(PreferKey.bookInfoInputColor) to 0x102030,
+        )
+
+        val settings = preferences.toThemeSettings()
+
+        assertEquals("accent", settings.customMode)
+        assertEquals(0x102030, settings.bookInfoInputColor)
+    }
+
+    @Test
+    fun `关闭模糊通过真实原子路径同时关闭渐进模糊`() {
+        val values = captureAtomicUpdateValues(
+            current = ThemeSettings(enableBlur = true, enableProgressiveBlur = true),
+            read = { it.toThemeSettings() },
+            toPrefMap = ThemeSettings::toGatewayPrefMap,
+            transform = {
+                it.copy(enableBlur = false, enableProgressiveBlur = false)
+            },
+        )
+
+        assertEquals(
+            mapOf(
+                PreferKey.enableBlur to false,
+                PreferKey.enableProgressiveBlur to false,
+            ),
+            values,
+        )
+    }
+
+    @Test
+    fun `Miuix Monet 根据原子路径当前主题决定是否回退`() {
+        fun enableMiuixMonet(current: ThemeSettings) = captureAtomicUpdateValues(
+            current = current,
+            read = { it.toThemeSettings() },
+            toPrefMap = ThemeSettings::toGatewayPrefMap,
+            transform = {
+                it.copy(
+                    useMiuixMonet = true,
+                    appTheme = if (it.appTheme != "0" && it.appTheme != "12") {
+                        "0"
+                    } else {
+                        it.appTheme
+                    },
+                )
+            },
+        )
+
+        assertEquals(
+            mapOf(
+                PreferKey.appTheme to "0",
+                PreferKey.useMiuixMonet to true,
+            ),
+            enableMiuixMonet(ThemeSettings(appTheme = "7")),
+        )
+        assertEquals(
+            mapOf(PreferKey.useMiuixMonet to true),
+            enableMiuixMonet(ThemeSettings(appTheme = "12")),
+        )
+    }
+
+    @Test
+    fun `透明主题通过真实原子路径同时写主题与容器透明度`() {
+        val values = captureAtomicUpdateValues(
+            current = ThemeSettings(appTheme = "0", containerOpacity = 80),
+            read = { it.toThemeSettings() },
+            toPrefMap = ThemeSettings::toGatewayPrefMap,
+            transform = { it.copy(appTheme = "13", containerOpacity = 0) },
+        )
+
+        assertEquals(
+            mapOf(
+                PreferKey.appTheme to "13",
+                PreferKey.containerOpacity to 0,
+            ),
+            values,
+        )
+    }
+
+    @Test
+    fun `清空背景路径通过真实原子路径删除对应键`() {
+        val values = captureAtomicUpdateValues(
+            current = ThemeSettings(backgroundImageLight = "old-background"),
+            read = { it.toThemeSettings() },
+            toPrefMap = ThemeSettings::toGatewayPrefMap,
+            transform = { it.copy(backgroundImageLight = null) },
+        )
+
+        assertEquals(mapOf(PreferKey.bgImage to null), values)
+    }
+}
+
+private fun themeMappingSamples(): List<ThemeSettings> {
+    val base = ThemeSettings(
+        appTheme = "app-theme",
+        useMiuixMonet = false,
+        isPureBlack = false,
+        paletteStyle = "palette-style",
+        materialVersion = "material-version",
+        customContrast = "custom-contrast",
+        customMode = "tonalSpot",
+        appFontPath = "app-font",
+        customPrimary = 101,
+        customNightPrimary = 102,
+        enableDeepPersonalization = false,
+        themeColor = 103,
+        secondaryThemeColor = 104,
+        primaryTextColor = 105,
+        secondaryTextColor = 106,
+        themeBackgroundColor = 107,
+        labelContainerColor = 108,
+        themeColorNight = 109,
+        secondaryThemeColorNight = 110,
+        primaryTextColorNight = 111,
+        secondaryTextColorNight = 112,
+        themeBackgroundColorNight = 113,
+        labelContainerColorNight = 114,
+        containerOpacity = 115,
+        overrideBaseCardCornerRadius = false,
+        baseCardCornerRadius = 1.25f,
+        overrideBaseCardBorder = false,
+        baseCardBorderWidth = 2.5f,
+        baseCardBorderColor = 116,
+        baseCardBorderColorNight = 117,
+        disableSplicedColumnGroupCornerRadius = false,
+        topBarOpacity = 118,
+        bottomBarOpacity = 119,
+        enableBlur = false,
+        enableProgressiveBlur = false,
+        topBarBlurRadius = 120,
+        bottomBarBlurRadius = 121,
+        topBarBlurAlpha = 122,
+        bottomBarBlurAlpha = 123,
+        bottomBarLensRadius = 3.75f,
+        useFlexibleTopAppBar = false,
+        bookInfoFollowCoverColor = false,
+        bookInfoNetworkCoverBackground = "network-background",
+        bookInfoDefaultCoverBackground = "default-background",
+        bookInfoInputColor = 0,
+        backgroundImageLight = "light-background",
+        backgroundImageDark = "dark-background",
+        backgroundImageBlurring = 124,
+        backgroundImageDarkBlurring = 125,
+        enableItemDivider = false,
+        itemDividerWidth = 4.5f,
+        itemDividerLength = 5.75f,
+        itemDividerColor = 126,
+        eyeProtectionEnabled = false,
+        colorTemperature = 127,
+        eyeProtectionSchedule = false,
+        eyeProtectionStartTime = "start-time",
+        eyeProtectionEndTime = "end-time",
+        showRefactorTip = false,
+        enableCustomTagColors = false,
+        customTagColorsJson = "tag-colors",
+    )
+    return listOf(
+        base,
+        base.copy(useMiuixMonet = true),
+        base.copy(isPureBlack = true),
+        base.copy(enableDeepPersonalization = true),
+        base.copy(overrideBaseCardCornerRadius = true),
+        base.copy(overrideBaseCardBorder = true),
+        base.copy(disableSplicedColumnGroupCornerRadius = true),
+        base.copy(enableBlur = true),
+        base.copy(enableProgressiveBlur = true),
+        base.copy(useFlexibleTopAppBar = true),
+        base.copy(bookInfoFollowCoverColor = true),
+        base.copy(enableItemDivider = true),
+        base.copy(eyeProtectionEnabled = true),
+        base.copy(eyeProtectionSchedule = true),
+        base.copy(showRefactorTip = true),
+        base.copy(enableCustomTagColors = true),
+    )
+}
+
+private fun ThemeSettings.expectedGatewayPrefMap(): Map<String, Any?> = mapOf(
+    PreferKey.appTheme to appTheme,
+    PreferKey.useMiuixMonet to useMiuixMonet,
+    PreferKey.pureBlack to isPureBlack,
+    PreferKey.paletteStyle to paletteStyle,
+    PreferKey.materialVersion to materialVersion,
+    PreferKey.customContrast to customContrast,
+    PreferKey.appFontPath to appFontPath,
+    PreferKey.cPrimary to customPrimary,
+    PreferKey.cNPrimary to customNightPrimary,
+    PreferKey.enableDeepPersonalization to enableDeepPersonalization,
+    PreferKey.themeColor to themeColor,
+    PreferKey.secondaryThemeColor to secondaryThemeColor,
+    PreferKey.primaryTextColor to primaryTextColor,
+    PreferKey.secondaryTextColor to secondaryTextColor,
+    PreferKey.themeBackgroundColor to themeBackgroundColor,
+    PreferKey.labelContainerColor to labelContainerColor,
+    PreferKey.themeColorNight to themeColorNight,
+    PreferKey.secondaryThemeColorNight to secondaryThemeColorNight,
+    PreferKey.primaryTextColorNight to primaryTextColorNight,
+    PreferKey.secondaryTextColorNight to secondaryTextColorNight,
+    PreferKey.themeBackgroundColorNight to themeBackgroundColorNight,
+    PreferKey.labelContainerColorNight to labelContainerColorNight,
+    PreferKey.containerOpacity to containerOpacity,
+    PreferKey.overrideBaseCardCornerRadius to overrideBaseCardCornerRadius,
+    PreferKey.baseCardCornerRadius to baseCardCornerRadius,
+    PreferKey.overrideBaseCardBorder to overrideBaseCardBorder,
+    PreferKey.baseCardBorderWidth to baseCardBorderWidth,
+    PreferKey.baseCardBorderColor to baseCardBorderColor,
+    PreferKey.baseCardBorderColorNight to baseCardBorderColorNight,
+    PreferKey.disableSplicedColumnGroupCornerRadius to disableSplicedColumnGroupCornerRadius,
+    PreferKey.topBarOpacity to topBarOpacity,
+    PreferKey.bottomBarOpacity to bottomBarOpacity,
+    PreferKey.enableBlur to enableBlur,
+    PreferKey.enableProgressiveBlur to enableProgressiveBlur,
+    PreferKey.topBarBlurRadius to topBarBlurRadius,
+    PreferKey.bottomBarBlurRadius to bottomBarBlurRadius,
+    PreferKey.topBarBlurAlpha to topBarBlurAlpha,
+    PreferKey.bottomBarBlurAlpha to bottomBarBlurAlpha,
+    PreferKey.bottomBarLensRadius to bottomBarLensRadius,
+    PreferKey.useFlexibleTopAppBar to useFlexibleTopAppBar,
+    PreferKey.bookInfoFollowCoverColor to bookInfoFollowCoverColor,
+    PreferKey.bookInfoNetworkCoverBackground to bookInfoNetworkCoverBackground,
+    PreferKey.bookInfoDefaultCoverBackground to bookInfoDefaultCoverBackground,
+    PreferKey.bgImage to backgroundImageLight,
+    PreferKey.bgImageN to backgroundImageDark,
+    PreferKey.bgImageBlurring to backgroundImageBlurring,
+    PreferKey.bgImageNBlurring to backgroundImageDarkBlurring,
+    PreferKey.enableItemDivider to enableItemDivider,
+    PreferKey.itemDividerWidth to itemDividerWidth,
+    PreferKey.itemDividerLength to itemDividerLength,
+    PreferKey.itemDividerColor to itemDividerColor,
+    PreferKey.eyeProtectionEnabled to eyeProtectionEnabled,
+    PreferKey.colorTemperature to colorTemperature,
+    PreferKey.eyeProtectionSchedule to eyeProtectionSchedule,
+    PreferKey.eyeProtectionStartTime to eyeProtectionStartTime,
+    PreferKey.eyeProtectionEndTime to eyeProtectionEndTime,
+    LocalPreferencesKeys.SHOW_THEME_REFACTOR_TIP.name to showRefactorTip,
+    PreferKey.enableCustomTagColors to enableCustomTagColors,
+    PreferKey.customTagColors to customTagColorsJson,
+)