Kaynağa Gözat

refactor: 完成剩余配置模块的 UDF 改造

新增备份、封面、下载缓存、实验室和翻译设置的强类型 Gateway、设置模型与 Contract。

将主题配置、主题管理及相关设置页面拆分为 Route/Screen,并统一通过 UiState、Intent 和 Effect 驱动。
5151561 3 ay önce
ebeveyn
işleme
8435aa9ed4
42 değiştirilmiş dosya ile 2651 ekleme ve 982 silme
  1. 320 5
      app/src/main/java/io/legado/app/data/repository/FeatureSettingsRepositories.kt
  2. 19 0
      app/src/main/java/io/legado/app/di/appModule.kt
  3. 34 0
      app/src/main/java/io/legado/app/domain/gateway/AppShellSettingsGateway.kt
  4. 23 0
      app/src/main/java/io/legado/app/domain/gateway/BackupSettingsGateway.kt
  5. 23 0
      app/src/main/java/io/legado/app/domain/gateway/CoverSettingsGateway.kt
  6. 19 0
      app/src/main/java/io/legado/app/domain/gateway/DownloadCacheSettingsGateway.kt
  7. 15 0
      app/src/main/java/io/legado/app/domain/gateway/LabSettingsGateway.kt
  8. 58 0
      app/src/main/java/io/legado/app/domain/gateway/ThemeSettingsGateway.kt
  9. 15 0
      app/src/main/java/io/legado/app/domain/gateway/TranslationSettingsGateway.kt
  10. 3 0
      app/src/main/java/io/legado/app/domain/model/settings/AppShellSettings.kt
  11. 15 0
      app/src/main/java/io/legado/app/domain/model/settings/BackupSettings.kt
  12. 19 0
      app/src/main/java/io/legado/app/domain/model/settings/CoverSettings.kt
  13. 11 0
      app/src/main/java/io/legado/app/domain/model/settings/DownloadCacheSettings.kt
  14. 7 0
      app/src/main/java/io/legado/app/domain/model/settings/LabSettings.kt
  15. 29 0
      app/src/main/java/io/legado/app/domain/model/settings/ThemeSettings.kt
  16. 9 0
      app/src/main/java/io/legado/app/domain/model/settings/TranslationSettings.kt
  17. 86 0
      app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigContract.kt
  18. 255 51
      app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt
  19. 49 0
      app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigContract.kt
  20. 116 69
      app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigScreen.kt
  21. 80 38
      app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigViewModel.kt
  22. 11 57
      app/src/main/java/io/legado/app/ui/config/coverConfig/CoverRuleConfigSheet.kt
  23. 34 0
      app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfigContract.kt
  24. 89 56
      app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfigScreen.kt
  25. 79 39
      app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfigViewModel.kt
  26. 17 0
      app/src/main/java/io/legado/app/ui/config/labConfig/LabConfigContract.kt
  27. 43 41
      app/src/main/java/io/legado/app/ui/config/labConfig/LabConfigScreen.kt
  28. 39 0
      app/src/main/java/io/legado/app/ui/config/labConfig/LabConfigViewModel.kt
  29. 5 27
      app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt
  30. 15 28
      app/src/main/java/io/legado/app/ui/config/themeConfig/MainNavigationSettingsSheet.kt
  31. 13 41
      app/src/main/java/io/legado/app/ui/config/themeConfig/NavIconManageSheet.kt
  32. 78 0
      app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigContract.kt
  33. 130 0
      app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigRouteScreen.kt
  34. 338 262
      app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt
  35. 259 82
      app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt
  36. 101 0
      app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageRouteScreen.kt
  37. 47 146
      app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageScreen.kt
  38. 40 1
      app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageViewModel.kt
  39. 17 0
      app/src/main/java/io/legado/app/ui/config/translation/TranslationConfigContract.kt
  40. 39 27
      app/src/main/java/io/legado/app/ui/config/translation/TranslationConfigScreen.kt
  41. 40 0
      app/src/main/java/io/legado/app/ui/config/translation/TranslationConfigViewModel.kt
  42. 12 12
      app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt

+ 320 - 5
app/src/main/java/io/legado/app/data/repository/FeatureSettingsRepositories.kt

@@ -1,18 +1,42 @@
 package io.legado.app.data.repository
 package io.legado.app.data.repository
 
 
 import io.legado.app.constant.PreferKey
 import io.legado.app.constant.PreferKey
+import io.legado.app.BuildConfig
 import io.legado.app.domain.gateway.AppShellSettingsGateway
 import io.legado.app.domain.gateway.AppShellSettingsGateway
+import io.legado.app.domain.gateway.AppShellBooleanSetting
+import io.legado.app.domain.gateway.AppShellStringSetting
 import io.legado.app.domain.gateway.AppShellSettingsUpdate
 import io.legado.app.domain.gateway.AppShellSettingsUpdate
 import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.domain.gateway.OtherSettingsUpdate
 import io.legado.app.domain.gateway.ThemeSettingsGateway
 import io.legado.app.domain.gateway.ThemeSettingsGateway
+import io.legado.app.domain.gateway.ThemeBooleanSetting
 import io.legado.app.domain.gateway.ThemeColorSlot
 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.ThemeStringSetting
 import io.legado.app.domain.gateway.ThemeSettingsUpdate
 import io.legado.app.domain.gateway.ThemeSettingsUpdate
 import io.legado.app.domain.model.settings.AppShellSettings
 import io.legado.app.domain.model.settings.AppShellSettings
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
+import io.legado.app.domain.gateway.DownloadCacheSettingsUpdate
+import io.legado.app.domain.model.settings.DownloadCacheSettings
+import io.legado.app.domain.gateway.CoverSettingsGateway
+import io.legado.app.domain.gateway.CoverSettingsUpdate
+import io.legado.app.domain.model.settings.CoverSettings
 import io.legado.app.domain.model.settings.OtherSettings
 import io.legado.app.domain.model.settings.OtherSettings
 import io.legado.app.domain.model.settings.ThemeSettings
 import io.legado.app.domain.model.settings.ThemeSettings
+import io.legado.app.domain.gateway.LabSettingsGateway
+import io.legado.app.domain.gateway.LabSettingsUpdate
+import io.legado.app.domain.gateway.TranslationSettingsGateway
+import io.legado.app.domain.gateway.TranslationSettingsUpdate
+import io.legado.app.domain.model.TranslationConstants
+import io.legado.app.domain.model.settings.LabSettings
+import io.legado.app.domain.model.settings.TranslationSettings
+import io.legado.app.domain.gateway.BackupSettingsGateway
+import io.legado.app.domain.gateway.BackupSettingsUpdate
+import io.legado.app.domain.model.settings.BackupSettings
 import io.legado.app.help.config.AppConfigStore
 import io.legado.app.help.config.AppConfigStore
 import io.legado.app.help.config.compatDsBoolean
 import io.legado.app.help.config.compatDsBoolean
+import io.legado.app.help.config.compatDsFloat
 import io.legado.app.help.config.compatDsInt
 import io.legado.app.help.config.compatDsInt
 import io.legado.app.help.config.compatDsString
 import io.legado.app.help.config.compatDsString
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.Preferences
@@ -28,15 +52,44 @@ class AppShellSettingsRepository : AppShellSettingsGateway {
         .map(Preferences::toAppShellSettings)
         .map(Preferences::toAppShellSettings)
         .distinctUntilChanged()
         .distinctUntilChanged()
 
 
-    override suspend fun update(update: AppShellSettingsUpdate) {
-        val (key, value) = when (update) {
+    override suspend fun update(update: AppShellSettingsUpdate) = updateAll(listOf(update))
+
+    override suspend fun updateAll(updates: List<AppShellSettingsUpdate>) {
+        val values = updates.associate { update ->
+            val (key, value) = when (update) {
             is AppShellSettingsUpdate.ThemeMode -> PreferKey.themeMode to update.value
             is AppShellSettingsUpdate.ThemeMode -> PreferKey.themeMode to update.value
             is AppShellSettingsUpdate.Language -> PreferKey.language to update.value
             is AppShellSettingsUpdate.Language -> PreferKey.language to update.value
             is AppShellSettingsUpdate.FontScale -> PreferKey.fontScale to update.value
             is AppShellSettingsUpdate.FontScale -> PreferKey.fontScale to update.value
             is AppShellSettingsUpdate.ComposeEngine -> PreferKey.composeEngine to update.value
             is AppShellSettingsUpdate.ComposeEngine -> PreferKey.composeEngine to update.value
             is AppShellSettingsUpdate.MainNavigationOrder -> PreferKey.mainNavigationOrder to update.value
             is AppShellSettingsUpdate.MainNavigationOrder -> PreferKey.mainNavigationOrder to update.value
+            is AppShellSettingsUpdate.BooleanValue -> when (update.setting) {
+                AppShellBooleanSetting.ShowHome -> PreferKey.showHome to update.value
+                AppShellBooleanSetting.ShowDiscovery -> PreferKey.showDiscovery to update.value
+                AppShellBooleanSetting.ShowRss -> PreferKey.showRss to update.value
+                AppShellBooleanSetting.ShowStatusBar -> PreferKey.showStatusBar to update.value
+                AppShellBooleanSetting.SwipeAnimation -> PreferKey.swipeAnimation to update.value
+                AppShellBooleanSetting.PredictiveBack -> PreferKey.isPredictiveBackEnabled to update.value
+                AppShellBooleanSetting.ShowBottomView -> PreferKey.showBottomView to update.value
+                AppShellBooleanSetting.UseFloatingBottomBar -> PreferKey.useFloatingBottomBar to update.value
+                AppShellBooleanSetting.UseFloatingBottomBarLiquidGlass ->
+                    PreferKey.useFloatingBottomBarLiquidGlass to update.value
+                AppShellBooleanSetting.NavExtended -> "navExtended" to update.value
+            }
+            is AppShellSettingsUpdate.StringValue -> when (update.setting) {
+                AppShellStringSetting.TabletInterface -> PreferKey.tabletInterface to update.value
+                AppShellStringSetting.LabelVisibilityMode -> PreferKey.labelVisibilityMode to update.value
+                AppShellStringSetting.DefaultHomePage -> PreferKey.defaultHomePage to update.value
+                AppShellStringSetting.NavIconHome -> PreferKey.navIconHome to update.value
+                AppShellStringSetting.NavIconBookshelf -> PreferKey.navIconBookshelf to update.value
+                AppShellStringSetting.NavIconExplore -> PreferKey.navIconExplore to update.value
+                AppShellStringSetting.NavIconRss -> PreferKey.navIconRss to update.value
+                AppShellStringSetting.NavIconMy -> PreferKey.navIconMy to update.value
+                AppShellStringSetting.LauncherIcon -> PreferKey.launcherIcon to update.value
+            }
+            }
+            key to value
         }
         }
-        AppConfigStore.putAll(mapOf(key to value))
+        AppConfigStore.putAll(values)
     }
     }
 }
 }
 
 
@@ -48,8 +101,11 @@ class ThemeSettingsRepository : ThemeSettingsGateway {
         .map(Preferences::toThemeSettings)
         .map(Preferences::toThemeSettings)
         .distinctUntilChanged()
         .distinctUntilChanged()
 
 
-    override suspend fun update(update: ThemeSettingsUpdate) {
-        val (key, value) = when (update) {
+    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.AppTheme -> PreferKey.appTheme to update.value
             is ThemeSettingsUpdate.PureBlack -> PreferKey.pureBlack to update.value
             is ThemeSettingsUpdate.PureBlack -> PreferKey.pureBlack to update.value
             is ThemeSettingsUpdate.PaletteStyle -> PreferKey.paletteStyle to update.value
             is ThemeSettingsUpdate.PaletteStyle -> PreferKey.paletteStyle to update.value
@@ -74,11 +130,234 @@ class ThemeSettingsRepository : ThemeSettingsGateway {
             is ThemeSettingsUpdate.AppFontPath -> PreferKey.appFontPath to update.value
             is ThemeSettingsUpdate.AppFontPath -> PreferKey.appFontPath to update.value
             is ThemeSettingsUpdate.CustomPrimary -> PreferKey.cPrimary to update.value
             is ThemeSettingsUpdate.CustomPrimary -> PreferKey.cPrimary to update.value
             is ThemeSettingsUpdate.CustomNightPrimary -> PreferKey.cNPrimary 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.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
+            }
+            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.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
+            }
+            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
+            }
+            }
+            key to value
+        }
+        AppConfigStore.putAll(values)
+    }
+}
+
+class DownloadCacheSettingsRepository : DownloadCacheSettingsGateway {
+    override val settings: Flow<DownloadCacheSettings> = AppConfigStore.preferencesFlow
+        .map { preferences ->
+            DownloadCacheSettings(
+                bitmapCacheSize = preferences.compatDsInt(PreferKey.bitmapCacheSize) ?: 50,
+                imageRetainNum = preferences.compatDsInt(PreferKey.imageRetainNum) ?: 0,
+                preDownloadNum = preferences.compatDsInt(PreferKey.preDownloadNum) ?: 10,
+                threadCount = preferences.compatDsInt(PreferKey.threadCount) ?: 16,
+                cacheBookThreadCount =
+                    preferences.compatDsInt(PreferKey.cacheBookThreadCount) ?: 16,
+                userAgent = preferences.compatDsString(PreferKey.userAgent).orEmpty().ifBlank {
+                    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
+                        "AppleWebKit/537.36 (KHTML, like Gecko) " +
+                        "Chrome/${BuildConfig.Cronet_Main_Version} Safari/537.36"
+                },
+                cronetEnabled = preferences.compatDsBoolean(PreferKey.cronet) ?: false,
+            )
+        }
+        .distinctUntilChanged()
+
+    override suspend fun update(update: DownloadCacheSettingsUpdate) {
+        val (key, value) = when (update) {
+            is DownloadCacheSettingsUpdate.BitmapCacheSize -> PreferKey.bitmapCacheSize to update.value
+            is DownloadCacheSettingsUpdate.ImageRetainNum -> PreferKey.imageRetainNum to update.value
+            is DownloadCacheSettingsUpdate.PreDownloadNum -> PreferKey.preDownloadNum to update.value
+            is DownloadCacheSettingsUpdate.ThreadCount -> PreferKey.threadCount to update.value
+            is DownloadCacheSettingsUpdate.CacheBookThreadCount ->
+                PreferKey.cacheBookThreadCount to update.value
+            is DownloadCacheSettingsUpdate.UserAgent -> PreferKey.userAgent to update.value
+            is DownloadCacheSettingsUpdate.CronetEnabled -> PreferKey.cronet to update.value
+        }
+        AppConfigStore.putAll(mapOf(key to value))
+    }
+}
+
+class CoverSettingsRepository : CoverSettingsGateway {
+    override val settings: Flow<CoverSettings> = AppConfigStore.preferencesFlow
+        .map { preferences ->
+            CoverSettings(
+                loadOnlyOnWifi = preferences.compatDsBoolean(PreferKey.loadCoverOnlyWifi) ?: false,
+                useDefaultCover = preferences.compatDsBoolean(PreferKey.useDefaultCover) ?: false,
+                showShadow = preferences.compatDsBoolean(PreferKey.coverShowShadow) ?: false,
+                showStroke = preferences.compatDsBoolean(PreferKey.coverShowStroke) ?: true,
+                useDefaultColor = preferences.compatDsBoolean(PreferKey.coverDefaultColor) ?: true,
+                textColor = preferences.compatDsInt(PreferKey.coverTextColor) ?: -16777216,
+                shadowColor = preferences.compatDsInt(PreferKey.coverShadowColor) ?: -16777216,
+                showName = preferences.compatDsBoolean(PreferKey.coverShowName) ?: true,
+                showAuthor = preferences.compatDsBoolean(PreferKey.coverShowAuthor) ?: true,
+                textColorDark = preferences.compatDsInt(PreferKey.coverTextColorN) ?: -1,
+                shadowColorDark = preferences.compatDsInt(PreferKey.coverShadowColorN) ?: -1,
+                showNameDark = preferences.compatDsBoolean(PreferKey.coverShowNameN) ?: true,
+                showAuthorDark = preferences.compatDsBoolean(PreferKey.coverShowAuthorN) ?: true,
+                infoOrientation = preferences.compatDsString(PreferKey.coverInfoOrientation) ?: "0",
+                exploreFilterState = preferences.compatDsInt(PreferKey.exploreFilterState) ?: 0,
+            )
+        }
+        .distinctUntilChanged()
+
+    override suspend fun update(update: CoverSettingsUpdate) {
+        val (key, value) = when (update) {
+            is CoverSettingsUpdate.LoadOnlyOnWifi -> PreferKey.loadCoverOnlyWifi to update.value
+            is CoverSettingsUpdate.UseDefaultCover -> PreferKey.useDefaultCover to update.value
+            is CoverSettingsUpdate.ShowShadow -> PreferKey.coverShowShadow to update.value
+            is CoverSettingsUpdate.ShowStroke -> PreferKey.coverShowStroke to update.value
+            is CoverSettingsUpdate.UseDefaultColor -> PreferKey.coverDefaultColor to update.value
+            is CoverSettingsUpdate.TextColor ->
+                (if (update.dark) PreferKey.coverTextColorN else PreferKey.coverTextColor) to update.value
+            is CoverSettingsUpdate.ShadowColor ->
+                (if (update.dark) PreferKey.coverShadowColorN else PreferKey.coverShadowColor) to update.value
+            is CoverSettingsUpdate.ShowName ->
+                (if (update.dark) PreferKey.coverShowNameN else PreferKey.coverShowName) to update.value
+            is CoverSettingsUpdate.ShowAuthor ->
+                (if (update.dark) PreferKey.coverShowAuthorN else PreferKey.coverShowAuthor) to update.value
+            is CoverSettingsUpdate.InfoOrientation -> PreferKey.coverInfoOrientation to update.value
+            is CoverSettingsUpdate.ExploreFilterState -> PreferKey.exploreFilterState to update.value
         }
         }
         AppConfigStore.putAll(mapOf(key to value))
         AppConfigStore.putAll(mapOf(key to value))
     }
     }
 }
 }
 
 
+class LabSettingsRepository : LabSettingsGateway {
+    override val settings: Flow<LabSettings> = AppConfigStore.preferencesFlow
+        .map { preferences ->
+            LabSettings(
+                enabled = preferences.compatDsBoolean(PreferKey.labEnabled) ?: false,
+                eInkDisplay = preferences.compatDsBoolean(PreferKey.labEInkDisplay) ?: false,
+                eyeProtection = preferences.compatDsBoolean(PreferKey.labEyeProtection) ?: false,
+            )
+        }
+        .distinctUntilChanged()
+
+    override suspend fun update(update: LabSettingsUpdate) {
+        val (key, value) = when (update) {
+            is LabSettingsUpdate.Enabled -> PreferKey.labEnabled to update.value
+            is LabSettingsUpdate.EInkDisplay -> PreferKey.labEInkDisplay to update.value
+            is LabSettingsUpdate.EyeProtection -> PreferKey.labEyeProtection to update.value
+        }
+        AppConfigStore.putAll(mapOf(key to value))
+    }
+}
+
+class TranslationSettingsRepository : TranslationSettingsGateway {
+    override val settings: Flow<TranslationSettings> = AppConfigStore.preferencesFlow
+        .map { preferences ->
+            val storedProvider = preferences.compatDsString(PreferKey.llmProvider)
+                ?: TranslationConstants.PROVIDER_GOOGLE
+            TranslationSettings(
+                provider = if (storedProvider == TranslationConstants.PROVIDER_OPENAI) {
+                    TranslationConstants.PROVIDER_APP_AI
+                } else {
+                    storedProvider
+                },
+                targetLanguage = preferences.compatDsString(PreferKey.llmTargetLanguage) ?: "zh",
+                maxCharsPerChunk = preferences.compatDsInt(PreferKey.llmMaxCharsPerChunk) ?: 10000,
+            )
+        }
+        .distinctUntilChanged()
+
+    override suspend fun update(update: TranslationSettingsUpdate) {
+        val (key, value) = when (update) {
+            is TranslationSettingsUpdate.Provider -> PreferKey.llmProvider to update.value
+            is TranslationSettingsUpdate.TargetLanguage -> PreferKey.llmTargetLanguage to update.value
+            is TranslationSettingsUpdate.MaxCharsPerChunk -> PreferKey.llmMaxCharsPerChunk to update.value
+        }
+        AppConfigStore.putAll(mapOf(key to value))
+    }
+}
+
+class BackupSettingsRepository : BackupSettingsGateway {
+    override val settings: Flow<BackupSettings> = AppConfigStore.preferencesFlow
+        .map { preferences ->
+            BackupSettings(
+                webDavUrl = preferences.compatDsString(PreferKey.webDavUrl).orEmpty(),
+                webDavAccount = preferences.compatDsString(PreferKey.webDavAccount).orEmpty(),
+                webDavPassword = preferences.compatDsString(PreferKey.webDavPassword).orEmpty(),
+                webDavDir = preferences.compatDsString(PreferKey.webDavDir) ?: "legado",
+                webDavDeviceName = preferences.compatDsString(PreferKey.webDavDeviceName).orEmpty(),
+                syncBookProgress = preferences.compatDsBoolean(PreferKey.syncBookProgress) ?: true,
+                syncBookProgressPlus =
+                    preferences.compatDsBoolean(PreferKey.syncBookProgressPlus) ?: false,
+                autoCheckNewBackup =
+                    preferences.compatDsBoolean(PreferKey.autoCheckNewBackup) ?: true,
+                onlyLatestBackup = preferences.compatDsBoolean(PreferKey.onlyLatestBackup) ?: true,
+                backupSyncMode = preferences.compatDsString(PreferKey.backupSyncMode) ?: "both",
+                backupPath = preferences.compatDsString(PreferKey.backupPath),
+            )
+        }
+        .distinctUntilChanged()
+
+    override suspend fun update(update: BackupSettingsUpdate) = updateAll(listOf(update))
+
+    override suspend fun updateAll(updates: List<BackupSettingsUpdate>) {
+        val values = buildMap {
+            updates.forEach { update ->
+                when (update) {
+                    is BackupSettingsUpdate.WebDavUrl -> put(PreferKey.webDavUrl, update.value)
+                    is BackupSettingsUpdate.WebDavCredentials -> {
+                        put(PreferKey.webDavAccount, update.account)
+                        put(PreferKey.webDavPassword, update.password)
+                    }
+                    is BackupSettingsUpdate.WebDavDir -> put(PreferKey.webDavDir, update.value)
+                    is BackupSettingsUpdate.WebDavDeviceName ->
+                        put(PreferKey.webDavDeviceName, update.value)
+                    is BackupSettingsUpdate.SyncBookProgress ->
+                        put(PreferKey.syncBookProgress, update.value)
+                    is BackupSettingsUpdate.SyncBookProgressPlus ->
+                        put(PreferKey.syncBookProgressPlus, update.value)
+                    is BackupSettingsUpdate.AutoCheckNewBackup ->
+                        put(PreferKey.autoCheckNewBackup, update.value)
+                    is BackupSettingsUpdate.OnlyLatestBackup ->
+                        put(PreferKey.onlyLatestBackup, update.value)
+                    is BackupSettingsUpdate.BackupSyncMode ->
+                        put(PreferKey.backupSyncMode, update.value)
+                    is BackupSettingsUpdate.BackupPath -> put(PreferKey.backupPath, update.value)
+                }
+            }
+        }
+        AppConfigStore.putAll(values)
+    }
+}
+
 private fun Preferences.toAppShellSettings(): AppShellSettings = AppShellSettings(
 private fun Preferences.toAppShellSettings(): AppShellSettings = AppShellSettings(
     themeMode = compatDsString(PreferKey.themeMode) ?: "0",
     themeMode = compatDsString(PreferKey.themeMode) ?: "0",
     language = compatDsString(PreferKey.language) ?: "auto",
     language = compatDsString(PreferKey.language) ?: "auto",
@@ -88,6 +367,8 @@ private fun Preferences.toAppShellSettings(): AppShellSettings = AppShellSetting
     showDiscovery = compatDsBoolean(PreferKey.showDiscovery) ?: true,
     showDiscovery = compatDsBoolean(PreferKey.showDiscovery) ?: true,
     showRss = compatDsBoolean(PreferKey.showRss) ?: true,
     showRss = compatDsBoolean(PreferKey.showRss) ?: true,
     showStatusBar = compatDsBoolean(PreferKey.showStatusBar) ?: true,
     showStatusBar = compatDsBoolean(PreferKey.showStatusBar) ?: true,
+    swipeAnimation = compatDsBoolean(PreferKey.swipeAnimation) ?: true,
+    predictiveBackEnabled = compatDsBoolean(PreferKey.isPredictiveBackEnabled) ?: true,
     showBottomView = compatDsBoolean(PreferKey.showBottomView) ?: true,
     showBottomView = compatDsBoolean(PreferKey.showBottomView) ?: true,
     useFloatingBottomBar = compatDsBoolean(PreferKey.useFloatingBottomBar) ?: false,
     useFloatingBottomBar = compatDsBoolean(PreferKey.useFloatingBottomBar) ?: false,
     useFloatingBottomBarLiquidGlass =
     useFloatingBottomBarLiquidGlass =
@@ -103,10 +384,12 @@ private fun Preferences.toAppShellSettings(): AppShellSettings = AppShellSetting
     navIconExplore = compatDsString(PreferKey.navIconExplore) ?: "",
     navIconExplore = compatDsString(PreferKey.navIconExplore) ?: "",
     navIconRss = compatDsString(PreferKey.navIconRss) ?: "",
     navIconRss = compatDsString(PreferKey.navIconRss) ?: "",
     navIconMy = compatDsString(PreferKey.navIconMy) ?: "",
     navIconMy = compatDsString(PreferKey.navIconMy) ?: "",
+    launcherIcon = compatDsString(PreferKey.launcherIcon) ?: "ic_launcher",
 )
 )
 
 
 private fun Preferences.toThemeSettings(): ThemeSettings = ThemeSettings(
 private fun Preferences.toThemeSettings(): ThemeSettings = ThemeSettings(
     appTheme = compatDsString(PreferKey.appTheme) ?: "0",
     appTheme = compatDsString(PreferKey.appTheme) ?: "0",
+    useMiuixMonet = compatDsBoolean(PreferKey.useMiuixMonet) ?: false,
     isPureBlack = compatDsBoolean(PreferKey.pureBlack) ?: false,
     isPureBlack = compatDsBoolean(PreferKey.pureBlack) ?: false,
     paletteStyle = compatDsString(PreferKey.paletteStyle) ?: "tonalSpot",
     paletteStyle = compatDsString(PreferKey.paletteStyle) ?: "tonalSpot",
     materialVersion = compatDsString(PreferKey.materialVersion) ?: "material3",
     materialVersion = compatDsString(PreferKey.materialVersion) ?: "material3",
@@ -127,6 +410,38 @@ private fun Preferences.toThemeSettings(): ThemeSettings = ThemeSettings(
     secondaryTextColorNight = compatDsInt(PreferKey.secondaryTextColorNight) ?: 0,
     secondaryTextColorNight = compatDsInt(PreferKey.secondaryTextColorNight) ?: 0,
     themeBackgroundColorNight = compatDsInt(PreferKey.themeBackgroundColorNight) ?: 0,
     themeBackgroundColorNight = compatDsInt(PreferKey.themeBackgroundColorNight) ?: 0,
     labelContainerColorNight = compatDsInt(PreferKey.labelContainerColorNight) ?: 0,
     labelContainerColorNight = compatDsInt(PreferKey.labelContainerColorNight) ?: 0,
+    containerOpacity = compatDsInt(PreferKey.containerOpacity) ?: 100,
+    topBarOpacity = compatDsInt(PreferKey.topBarOpacity) ?: 100,
+    bottomBarOpacity = compatDsInt(PreferKey.bottomBarOpacity) ?: 100,
+    enableBlur = compatDsBoolean(PreferKey.enableBlur) ?: false,
+    enableProgressiveBlur = compatDsBoolean(PreferKey.enableProgressiveBlur) ?: false,
+    topBarBlurRadius = compatDsInt(PreferKey.topBarBlurRadius) ?: 24,
+    bottomBarBlurRadius = compatDsInt(PreferKey.bottomBarBlurRadius) ?: 8,
+    topBarBlurAlpha = compatDsInt(PreferKey.topBarBlurAlpha) ?: 73,
+    bottomBarBlurAlpha = compatDsInt(PreferKey.bottomBarBlurAlpha) ?: 40,
+    bottomBarLensRadius = compatDsFloat(PreferKey.bottomBarLensRadius) ?: 24f,
+    useFlexibleTopAppBar = compatDsBoolean(PreferKey.useFlexibleTopAppBar) ?: true,
+    bookInfoFollowCoverColor = compatDsBoolean(PreferKey.bookInfoFollowCoverColor) ?: true,
+    bookInfoNetworkCoverBackground =
+        compatDsString(PreferKey.bookInfoNetworkCoverBackground) ?: "on",
+    bookInfoDefaultCoverBackground =
+        compatDsString(PreferKey.bookInfoDefaultCoverBackground) ?: "on",
+    backgroundImageLight = compatDsString(PreferKey.bgImage),
+    backgroundImageDark = compatDsString(PreferKey.bgImageN),
+    backgroundImageBlurring = compatDsInt(PreferKey.bgImageBlurring) ?: 0,
+    backgroundImageDarkBlurring = compatDsInt(PreferKey.bgImageNBlurring) ?: 0,
+    enableItemDivider = compatDsBoolean(PreferKey.enableItemDivider) ?: false,
+    itemDividerWidth = compatDsFloat(PreferKey.itemDividerWidth) ?: 1f,
+    itemDividerLength = compatDsFloat(PreferKey.itemDividerLength) ?: 80f,
+    itemDividerColor = compatDsInt(PreferKey.itemDividerColor) ?: 0,
+    eyeProtectionEnabled = compatDsBoolean(PreferKey.eyeProtectionEnabled) ?: false,
+    colorTemperature = compatDsInt(PreferKey.colorTemperature) ?: 50,
+    eyeProtectionSchedule = compatDsBoolean(PreferKey.eyeProtectionSchedule) ?: false,
+    eyeProtectionStartTime = compatDsString(PreferKey.eyeProtectionStartTime) ?: "22:00",
+    eyeProtectionEndTime = compatDsString(PreferKey.eyeProtectionEndTime) ?: "07:00",
+    showRefactorTip = compatDsBoolean(
+        io.legado.app.data.local.preferences.LocalPreferencesKeys.SHOW_THEME_REFACTOR_TIP.name
+    ) ?: true,
 )
 )
 
 
 class OtherSettingsRepository : OtherSettingsGateway {
 class OtherSettingsRepository : OtherSettingsGateway {

+ 19 - 0
app/src/main/java/io/legado/app/di/appModule.kt

@@ -16,6 +16,7 @@ import io.legado.app.data.repository.AiToolRepository
 import io.legado.app.data.repository.AppStartupRepository
 import io.legado.app.data.repository.AppStartupRepository
 import io.legado.app.data.repository.AppShellSettingsRepository
 import io.legado.app.data.repository.AppShellSettingsRepository
 import io.legado.app.data.repository.BackupRestoreRepository
 import io.legado.app.data.repository.BackupRestoreRepository
+import io.legado.app.data.repository.BackupSettingsRepository
 import io.legado.app.data.repository.BookCacheCleanupRepository
 import io.legado.app.data.repository.BookCacheCleanupRepository
 import io.legado.app.data.repository.BookContentProcessRepository
 import io.legado.app.data.repository.BookContentProcessRepository
 import io.legado.app.data.repository.BookDomainRepositoryImpl
 import io.legado.app.data.repository.BookDomainRepositoryImpl
@@ -26,15 +27,18 @@ import io.legado.app.data.repository.BookSourceRepository
 import io.legado.app.data.repository.BookshelfRepository
 import io.legado.app.data.repository.BookshelfRepository
 import io.legado.app.data.repository.CacheBookDownloadRepository
 import io.legado.app.data.repository.CacheBookDownloadRepository
 import io.legado.app.data.repository.CoverAlbumRepository
 import io.legado.app.data.repository.CoverAlbumRepository
+import io.legado.app.data.repository.CoverSettingsRepository
 import io.legado.app.data.repository.DatabaseMaintenanceRepository
 import io.legado.app.data.repository.DatabaseMaintenanceRepository
 import io.legado.app.data.repository.DictRuleRepository
 import io.legado.app.data.repository.DictRuleRepository
 import io.legado.app.data.repository.DictionaryRepositoryImpl
 import io.legado.app.data.repository.DictionaryRepositoryImpl
 import io.legado.app.data.repository.DirectLinkUploadRepository
 import io.legado.app.data.repository.DirectLinkUploadRepository
+import io.legado.app.data.repository.DownloadCacheSettingsRepository
 import io.legado.app.data.repository.ExploreRepository
 import io.legado.app.data.repository.ExploreRepository
 import io.legado.app.data.repository.ExploreRepositoryImpl
 import io.legado.app.data.repository.ExploreRepositoryImpl
 import io.legado.app.data.repository.HighlightRuleRepository
 import io.legado.app.data.repository.HighlightRuleRepository
 import io.legado.app.data.repository.HomeDashboardRepository
 import io.legado.app.data.repository.HomeDashboardRepository
 import io.legado.app.data.repository.HomepageModulesRepository
 import io.legado.app.data.repository.HomepageModulesRepository
+import io.legado.app.data.repository.LabSettingsRepository
 import io.legado.app.data.repository.LocalBookRepository
 import io.legado.app.data.repository.LocalBookRepository
 import io.legado.app.data.repository.OtherSettingsRepository
 import io.legado.app.data.repository.OtherSettingsRepository
 import io.legado.app.data.repository.ReadAloudSettingsRepository
 import io.legado.app.data.repository.ReadAloudSettingsRepository
@@ -52,6 +56,7 @@ import io.legado.app.data.repository.SettingsRepository
 import io.legado.app.data.repository.ThemeSettingsRepository
 import io.legado.app.data.repository.ThemeSettingsRepository
 import io.legado.app.data.repository.TxtTocRuleRepository
 import io.legado.app.data.repository.TxtTocRuleRepository
 import io.legado.app.data.repository.TranslationCacheRepositoryImpl
 import io.legado.app.data.repository.TranslationCacheRepositoryImpl
+import io.legado.app.data.repository.TranslationSettingsRepository
 import io.legado.app.data.repository.UploadRepository
 import io.legado.app.data.repository.UploadRepository
 import io.legado.app.data.repository.WebDavBackupRepository
 import io.legado.app.data.repository.WebDavBackupRepository
 import io.legado.app.data.repository.WebDavReadingProgressRepository
 import io.legado.app.data.repository.WebDavReadingProgressRepository
@@ -65,17 +70,21 @@ import io.legado.app.domain.gateway.AiToolGateway
 import io.legado.app.domain.gateway.AppStartupGateway
 import io.legado.app.domain.gateway.AppStartupGateway
 import io.legado.app.domain.gateway.AppShellSettingsGateway
 import io.legado.app.domain.gateway.AppShellSettingsGateway
 import io.legado.app.domain.gateway.BackupRestoreGateway
 import io.legado.app.domain.gateway.BackupRestoreGateway
+import io.legado.app.domain.gateway.BackupSettingsGateway
 import io.legado.app.domain.gateway.BookCacheCleanupGateway
 import io.legado.app.domain.gateway.BookCacheCleanupGateway
 import io.legado.app.domain.gateway.BookCacheDownloadGateway
 import io.legado.app.domain.gateway.BookCacheDownloadGateway
 import io.legado.app.domain.gateway.BookContentProcessGateway
 import io.legado.app.domain.gateway.BookContentProcessGateway
 import io.legado.app.domain.gateway.BookSearchGateway
 import io.legado.app.domain.gateway.BookSearchGateway
 import io.legado.app.domain.gateway.BookSourceCallbackGateway
 import io.legado.app.domain.gateway.BookSourceCallbackGateway
 import io.legado.app.domain.gateway.CoverAlbumGateway
 import io.legado.app.domain.gateway.CoverAlbumGateway
+import io.legado.app.domain.gateway.CoverSettingsGateway
 import io.legado.app.domain.gateway.DatabaseMaintenanceGateway
 import io.legado.app.domain.gateway.DatabaseMaintenanceGateway
 import io.legado.app.domain.gateway.DictionaryGateway
 import io.legado.app.domain.gateway.DictionaryGateway
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
 import io.legado.app.domain.gateway.ExploreBooksGateway
 import io.legado.app.domain.gateway.ExploreBooksGateway
 import io.legado.app.domain.gateway.HomeDashboardGateway
 import io.legado.app.domain.gateway.HomeDashboardGateway
 import io.legado.app.domain.gateway.HomepageModulesGateway
 import io.legado.app.domain.gateway.HomepageModulesGateway
+import io.legado.app.domain.gateway.LabSettingsGateway
 import io.legado.app.domain.gateway.LocalBookGateway
 import io.legado.app.domain.gateway.LocalBookGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.domain.gateway.OtherSettingsGateway
 import io.legado.app.domain.gateway.ReadingProgressGateway
 import io.legado.app.domain.gateway.ReadingProgressGateway
@@ -83,6 +92,7 @@ import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.domain.gateway.ReadAloudSettingsGateway
 import io.legado.app.domain.gateway.ReadAloudSettingsGateway
 import io.legado.app.domain.gateway.ThemeSettingsGateway
 import io.legado.app.domain.gateway.ThemeSettingsGateway
 import io.legado.app.domain.gateway.TranslationCacheGateway
 import io.legado.app.domain.gateway.TranslationCacheGateway
+import io.legado.app.domain.gateway.TranslationSettingsGateway
 import io.legado.app.domain.gateway.WebDavBackupGateway
 import io.legado.app.domain.gateway.WebDavBackupGateway
 import io.legado.app.domain.repository.BookDomainRepository
 import io.legado.app.domain.repository.BookDomainRepository
 import io.legado.app.domain.usecase.AddBookUseCase
 import io.legado.app.domain.usecase.AddBookUseCase
@@ -165,6 +175,8 @@ import io.legado.app.ui.config.customTheme.CustomThemeViewModel
 import io.legado.app.ui.config.readConfig.ReadConfigViewModel
 import io.legado.app.ui.config.readConfig.ReadConfigViewModel
 import io.legado.app.ui.config.themeConfig.ThemeConfigViewModel
 import io.legado.app.ui.config.themeConfig.ThemeConfigViewModel
 import io.legado.app.ui.config.themeManage.ThemeManageViewModel
 import io.legado.app.ui.config.themeManage.ThemeManageViewModel
+import io.legado.app.ui.config.labConfig.LabConfigViewModel
+import io.legado.app.ui.config.translation.TranslationConfigViewModel
 import io.legado.app.ui.dict.DictViewModel
 import io.legado.app.ui.dict.DictViewModel
 import io.legado.app.ui.dict.rule.DictRuleViewModel
 import io.legado.app.ui.dict.rule.DictRuleViewModel
 import io.legado.app.ui.highlightTagRule.HighlightTagRuleViewModel
 import io.legado.app.ui.highlightTagRule.HighlightTagRuleViewModel
@@ -214,6 +226,11 @@ val appModule = module {
     single<AppShellSettingsGateway> { AppShellSettingsRepository() }
     single<AppShellSettingsGateway> { AppShellSettingsRepository() }
     single<ThemeSettingsGateway> { ThemeSettingsRepository() }
     single<ThemeSettingsGateway> { ThemeSettingsRepository() }
     single<OtherSettingsGateway> { OtherSettingsRepository() }
     single<OtherSettingsGateway> { OtherSettingsRepository() }
+    single<DownloadCacheSettingsGateway> { DownloadCacheSettingsRepository() }
+    single<CoverSettingsGateway> { CoverSettingsRepository() }
+    single<BackupSettingsGateway> { BackupSettingsRepository() }
+    single<LabSettingsGateway> { LabSettingsRepository() }
+    single<TranslationSettingsGateway> { TranslationSettingsRepository() }
     singleOf(::ReadSettingsRepository)
     singleOf(::ReadSettingsRepository)
     single<ReadSettingsGateway> { get<ReadSettingsRepository>() }
     single<ReadSettingsGateway> { get<ReadSettingsRepository>() }
     singleOf(::ReadAloudSettingsRepository)
     singleOf(::ReadAloudSettingsRepository)
@@ -343,6 +360,8 @@ val appModule = module {
     viewModelOf(::ThemeConfigViewModel)
     viewModelOf(::ThemeConfigViewModel)
     viewModelOf(::ThemeManageViewModel)
     viewModelOf(::ThemeManageViewModel)
     viewModelOf(::BackupConfigViewModel)
     viewModelOf(::BackupConfigViewModel)
+    viewModelOf(::LabConfigViewModel)
+    viewModelOf(::TranslationConfigViewModel)
     viewModelOf(::AiConfigViewModel)
     viewModelOf(::AiConfigViewModel)
     viewModelOf(::AiSummaryConfigViewModel)
     viewModelOf(::AiSummaryConfigViewModel)
     viewModelOf(::AiChatViewModel)
     viewModelOf(::AiChatViewModel)

+ 34 - 0
app/src/main/java/io/legado/app/domain/gateway/AppShellSettingsGateway.kt

@@ -7,6 +7,7 @@ interface AppShellSettingsGateway {
     val currentSettings: AppShellSettings
     val currentSettings: AppShellSettings
     val settings: Flow<AppShellSettings>
     val settings: Flow<AppShellSettings>
     suspend fun update(update: AppShellSettingsUpdate)
     suspend fun update(update: AppShellSettingsUpdate)
+    suspend fun updateAll(updates: List<AppShellSettingsUpdate>)
 }
 }
 
 
 sealed interface AppShellSettingsUpdate {
 sealed interface AppShellSettingsUpdate {
@@ -15,4 +16,37 @@ sealed interface AppShellSettingsUpdate {
     data class FontScale(val value: Int) : AppShellSettingsUpdate
     data class FontScale(val value: Int) : AppShellSettingsUpdate
     data class ComposeEngine(val value: String) : AppShellSettingsUpdate
     data class ComposeEngine(val value: String) : AppShellSettingsUpdate
     data class MainNavigationOrder(val value: String) : AppShellSettingsUpdate
     data class MainNavigationOrder(val value: String) : AppShellSettingsUpdate
+    data class BooleanValue(
+        val setting: AppShellBooleanSetting,
+        val value: Boolean,
+    ) : AppShellSettingsUpdate
+    data class StringValue(
+        val setting: AppShellStringSetting,
+        val value: String,
+    ) : AppShellSettingsUpdate
+}
+
+enum class AppShellBooleanSetting {
+    ShowHome,
+    ShowDiscovery,
+    ShowRss,
+    ShowStatusBar,
+    SwipeAnimation,
+    PredictiveBack,
+    ShowBottomView,
+    UseFloatingBottomBar,
+    UseFloatingBottomBarLiquidGlass,
+    NavExtended,
+}
+
+enum class AppShellStringSetting {
+    TabletInterface,
+    LabelVisibilityMode,
+    DefaultHomePage,
+    NavIconHome,
+    NavIconBookshelf,
+    NavIconExplore,
+    NavIconRss,
+    NavIconMy,
+    LauncherIcon,
 }
 }

+ 23 - 0
app/src/main/java/io/legado/app/domain/gateway/BackupSettingsGateway.kt

@@ -0,0 +1,23 @@
+package io.legado.app.domain.gateway
+
+import io.legado.app.domain.model.settings.BackupSettings
+import kotlinx.coroutines.flow.Flow
+
+interface BackupSettingsGateway {
+    val settings: Flow<BackupSettings>
+    suspend fun update(update: BackupSettingsUpdate)
+    suspend fun updateAll(updates: List<BackupSettingsUpdate>)
+}
+
+sealed interface BackupSettingsUpdate {
+    data class WebDavUrl(val value: String) : BackupSettingsUpdate
+    data class WebDavCredentials(val account: String, val password: String) : BackupSettingsUpdate
+    data class WebDavDir(val value: String) : BackupSettingsUpdate
+    data class WebDavDeviceName(val value: String) : BackupSettingsUpdate
+    data class SyncBookProgress(val value: Boolean) : BackupSettingsUpdate
+    data class SyncBookProgressPlus(val value: Boolean) : BackupSettingsUpdate
+    data class AutoCheckNewBackup(val value: Boolean) : BackupSettingsUpdate
+    data class OnlyLatestBackup(val value: Boolean) : BackupSettingsUpdate
+    data class BackupSyncMode(val value: String) : BackupSettingsUpdate
+    data class BackupPath(val value: String?) : BackupSettingsUpdate
+}

+ 23 - 0
app/src/main/java/io/legado/app/domain/gateway/CoverSettingsGateway.kt

@@ -0,0 +1,23 @@
+package io.legado.app.domain.gateway
+
+import io.legado.app.domain.model.settings.CoverSettings
+import kotlinx.coroutines.flow.Flow
+
+interface CoverSettingsGateway {
+    val settings: Flow<CoverSettings>
+    suspend fun update(update: CoverSettingsUpdate)
+}
+
+sealed interface CoverSettingsUpdate {
+    data class LoadOnlyOnWifi(val value: Boolean) : CoverSettingsUpdate
+    data class UseDefaultCover(val value: Boolean) : CoverSettingsUpdate
+    data class ShowShadow(val value: Boolean) : CoverSettingsUpdate
+    data class ShowStroke(val value: Boolean) : CoverSettingsUpdate
+    data class UseDefaultColor(val value: Boolean) : CoverSettingsUpdate
+    data class TextColor(val value: Int, val dark: Boolean) : CoverSettingsUpdate
+    data class ShadowColor(val value: Int, val dark: Boolean) : CoverSettingsUpdate
+    data class ShowName(val value: Boolean, val dark: Boolean) : CoverSettingsUpdate
+    data class ShowAuthor(val value: Boolean, val dark: Boolean) : CoverSettingsUpdate
+    data class InfoOrientation(val value: String) : CoverSettingsUpdate
+    data class ExploreFilterState(val value: Int) : CoverSettingsUpdate
+}

+ 19 - 0
app/src/main/java/io/legado/app/domain/gateway/DownloadCacheSettingsGateway.kt

@@ -0,0 +1,19 @@
+package io.legado.app.domain.gateway
+
+import io.legado.app.domain.model.settings.DownloadCacheSettings
+import kotlinx.coroutines.flow.Flow
+
+interface DownloadCacheSettingsGateway {
+    val settings: Flow<DownloadCacheSettings>
+    suspend fun update(update: DownloadCacheSettingsUpdate)
+}
+
+sealed interface DownloadCacheSettingsUpdate {
+    data class BitmapCacheSize(val value: Int) : DownloadCacheSettingsUpdate
+    data class ImageRetainNum(val value: Int) : DownloadCacheSettingsUpdate
+    data class PreDownloadNum(val value: Int) : DownloadCacheSettingsUpdate
+    data class ThreadCount(val value: Int) : DownloadCacheSettingsUpdate
+    data class CacheBookThreadCount(val value: Int) : DownloadCacheSettingsUpdate
+    data class UserAgent(val value: String) : DownloadCacheSettingsUpdate
+    data class CronetEnabled(val value: Boolean) : DownloadCacheSettingsUpdate
+}

+ 15 - 0
app/src/main/java/io/legado/app/domain/gateway/LabSettingsGateway.kt

@@ -0,0 +1,15 @@
+package io.legado.app.domain.gateway
+
+import io.legado.app.domain.model.settings.LabSettings
+import kotlinx.coroutines.flow.Flow
+
+interface LabSettingsGateway {
+    val settings: Flow<LabSettings>
+    suspend fun update(update: LabSettingsUpdate)
+}
+
+sealed interface LabSettingsUpdate {
+    data class Enabled(val value: Boolean) : LabSettingsUpdate
+    data class EInkDisplay(val value: Boolean) : LabSettingsUpdate
+    data class EyeProtection(val value: Boolean) : LabSettingsUpdate
+}

+ 58 - 0
app/src/main/java/io/legado/app/domain/gateway/ThemeSettingsGateway.kt

@@ -7,6 +7,7 @@ interface ThemeSettingsGateway {
     val currentSettings: ThemeSettings
     val currentSettings: ThemeSettings
     val settings: Flow<ThemeSettings>
     val settings: Flow<ThemeSettings>
     suspend fun update(update: ThemeSettingsUpdate)
     suspend fun update(update: ThemeSettingsUpdate)
+    suspend fun updateAll(updates: List<ThemeSettingsUpdate>)
 }
 }
 
 
 sealed interface ThemeSettingsUpdate {
 sealed interface ThemeSettingsUpdate {
@@ -20,6 +21,63 @@ sealed interface ThemeSettingsUpdate {
     data class AppFontPath(val value: String?) : ThemeSettingsUpdate
     data class AppFontPath(val value: String?) : ThemeSettingsUpdate
     data class CustomPrimary(val value: Int) : ThemeSettingsUpdate
     data class CustomPrimary(val value: Int) : ThemeSettingsUpdate
     data class CustomNightPrimary(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,
+    EyeProtectionEnabled,
+    EyeProtectionSchedule,
+    ShowRefactorTip,
+}
+
+enum class ThemeIntSetting {
+    ContainerOpacity,
+    TopBarOpacity,
+    BottomBarOpacity,
+    TopBarBlurRadius,
+    BottomBarBlurRadius,
+    TopBarBlurAlpha,
+    BottomBarBlurAlpha,
+    BackgroundImageBlurring,
+    BackgroundImageDarkBlurring,
+    ItemDividerColor,
+    ColorTemperature,
+}
+
+enum class ThemeFloatSetting {
+    BottomBarLensRadius,
+    ItemDividerWidth,
+    ItemDividerLength,
+}
+
+enum class ThemeStringSetting {
+    BookInfoNetworkCoverBackground,
+    BookInfoDefaultCoverBackground,
+    BackgroundImageLight,
+    BackgroundImageDark,
+    EyeProtectionStartTime,
+    EyeProtectionEndTime,
 }
 }
 
 
 enum class ThemeColorSlot {
 enum class ThemeColorSlot {

+ 15 - 0
app/src/main/java/io/legado/app/domain/gateway/TranslationSettingsGateway.kt

@@ -0,0 +1,15 @@
+package io.legado.app.domain.gateway
+
+import io.legado.app.domain.model.settings.TranslationSettings
+import kotlinx.coroutines.flow.Flow
+
+interface TranslationSettingsGateway {
+    val settings: Flow<TranslationSettings>
+    suspend fun update(update: TranslationSettingsUpdate)
+}
+
+sealed interface TranslationSettingsUpdate {
+    data class Provider(val value: String) : TranslationSettingsUpdate
+    data class TargetLanguage(val value: String) : TranslationSettingsUpdate
+    data class MaxCharsPerChunk(val value: Int) : TranslationSettingsUpdate
+}

+ 3 - 0
app/src/main/java/io/legado/app/domain/model/settings/AppShellSettings.kt

@@ -9,6 +9,8 @@ data class AppShellSettings(
     val showDiscovery: Boolean = true,
     val showDiscovery: Boolean = true,
     val showRss: Boolean = true,
     val showRss: Boolean = true,
     val showStatusBar: Boolean = true,
     val showStatusBar: Boolean = true,
+    val swipeAnimation: Boolean = true,
+    val predictiveBackEnabled: Boolean = true,
     val showBottomView: Boolean = true,
     val showBottomView: Boolean = true,
     val useFloatingBottomBar: Boolean = false,
     val useFloatingBottomBar: Boolean = false,
     val useFloatingBottomBarLiquidGlass: Boolean = false,
     val useFloatingBottomBarLiquidGlass: Boolean = false,
@@ -22,4 +24,5 @@ data class AppShellSettings(
     val navIconExplore: String = "",
     val navIconExplore: String = "",
     val navIconRss: String = "",
     val navIconRss: String = "",
     val navIconMy: String = "",
     val navIconMy: String = "",
+    val launcherIcon: String = "ic_launcher",
 )
 )

+ 15 - 0
app/src/main/java/io/legado/app/domain/model/settings/BackupSettings.kt

@@ -0,0 +1,15 @@
+package io.legado.app.domain.model.settings
+
+data class BackupSettings(
+    val webDavUrl: String = "",
+    val webDavAccount: String = "",
+    val webDavPassword: String = "",
+    val webDavDir: String = "legado",
+    val webDavDeviceName: String = "",
+    val syncBookProgress: Boolean = true,
+    val syncBookProgressPlus: Boolean = false,
+    val autoCheckNewBackup: Boolean = true,
+    val onlyLatestBackup: Boolean = true,
+    val backupSyncMode: String = "both",
+    val backupPath: String? = null,
+)

+ 19 - 0
app/src/main/java/io/legado/app/domain/model/settings/CoverSettings.kt

@@ -0,0 +1,19 @@
+package io.legado.app.domain.model.settings
+
+data class CoverSettings(
+    val loadOnlyOnWifi: Boolean = false,
+    val useDefaultCover: Boolean = false,
+    val showShadow: Boolean = false,
+    val showStroke: Boolean = true,
+    val useDefaultColor: Boolean = true,
+    val textColor: Int = -16777216,
+    val shadowColor: Int = -16777216,
+    val showName: Boolean = true,
+    val showAuthor: Boolean = true,
+    val textColorDark: Int = -1,
+    val shadowColorDark: Int = -1,
+    val showNameDark: Boolean = true,
+    val showAuthorDark: Boolean = true,
+    val infoOrientation: String = "0",
+    val exploreFilterState: Int = 0,
+)

+ 11 - 0
app/src/main/java/io/legado/app/domain/model/settings/DownloadCacheSettings.kt

@@ -0,0 +1,11 @@
+package io.legado.app.domain.model.settings
+
+data class DownloadCacheSettings(
+    val bitmapCacheSize: Int = 50,
+    val imageRetainNum: Int = 0,
+    val preDownloadNum: Int = 10,
+    val threadCount: Int = 16,
+    val cacheBookThreadCount: Int = 16,
+    val userAgent: String = "",
+    val cronetEnabled: Boolean = false,
+)

+ 7 - 0
app/src/main/java/io/legado/app/domain/model/settings/LabSettings.kt

@@ -0,0 +1,7 @@
+package io.legado.app.domain.model.settings
+
+data class LabSettings(
+    val enabled: Boolean = false,
+    val eInkDisplay: Boolean = false,
+    val eyeProtection: Boolean = false,
+)

+ 29 - 0
app/src/main/java/io/legado/app/domain/model/settings/ThemeSettings.kt

@@ -2,6 +2,7 @@ package io.legado.app.domain.model.settings
 
 
 data class ThemeSettings(
 data class ThemeSettings(
     val appTheme: String = "0",
     val appTheme: String = "0",
+    val useMiuixMonet: Boolean = false,
     val isPureBlack: Boolean = false,
     val isPureBlack: Boolean = false,
     val paletteStyle: String = "tonalSpot",
     val paletteStyle: String = "tonalSpot",
     val materialVersion: String = "material3",
     val materialVersion: String = "material3",
@@ -22,6 +23,34 @@ data class ThemeSettings(
     val secondaryTextColorNight: Int = 0,
     val secondaryTextColorNight: Int = 0,
     val themeBackgroundColorNight: Int = 0,
     val themeBackgroundColorNight: Int = 0,
     val labelContainerColorNight: Int = 0,
     val labelContainerColorNight: Int = 0,
+    val containerOpacity: Int = 100,
+    val topBarOpacity: Int = 100,
+    val bottomBarOpacity: Int = 100,
+    val enableBlur: Boolean = false,
+    val enableProgressiveBlur: Boolean = false,
+    val topBarBlurRadius: Int = 24,
+    val bottomBarBlurRadius: Int = 8,
+    val topBarBlurAlpha: Int = 73,
+    val bottomBarBlurAlpha: Int = 40,
+    val bottomBarLensRadius: Float = 24f,
+    val useFlexibleTopAppBar: Boolean = true,
+    val bookInfoFollowCoverColor: Boolean = true,
+    val bookInfoNetworkCoverBackground: String = "on",
+    val bookInfoDefaultCoverBackground: String = "on",
+    val backgroundImageLight: String? = null,
+    val backgroundImageDark: String? = null,
+    val backgroundImageBlurring: Int = 0,
+    val backgroundImageDarkBlurring: Int = 0,
+    val enableItemDivider: Boolean = false,
+    val itemDividerWidth: Float = 1f,
+    val itemDividerLength: Float = 80f,
+    val itemDividerColor: Int = 0,
+    val eyeProtectionEnabled: Boolean = false,
+    val colorTemperature: Int = 50,
+    val eyeProtectionSchedule: Boolean = false,
+    val eyeProtectionStartTime: String = "22:00",
+    val eyeProtectionEndTime: String = "07:00",
+    val showRefactorTip: Boolean = true,
 )
 )
 
 
 data class ThemeCustomColors(
 data class ThemeCustomColors(

+ 9 - 0
app/src/main/java/io/legado/app/domain/model/settings/TranslationSettings.kt

@@ -0,0 +1,9 @@
+package io.legado.app.domain.model.settings
+
+import io.legado.app.domain.model.TranslationConstants
+
+data class TranslationSettings(
+    val provider: String = TranslationConstants.PROVIDER_GOOGLE,
+    val targetLanguage: String = "zh",
+    val maxCharsPerChunk: Int = 10000,
+)

+ 86 - 0
app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigContract.kt

@@ -0,0 +1,86 @@
+package io.legado.app.ui.config.backupConfig
+
+import androidx.annotation.StringRes
+import androidx.compose.runtime.Stable
+import io.legado.app.domain.model.settings.BackupSettings
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+
+@Stable
+data class BackupConfigUiState(
+    val settings: BackupSettings = BackupSettings(),
+    val activeSheet: BackupConfigSheet? = null,
+    val activeDialog: BackupConfigDialog? = null,
+    val backupNames: ImmutableList<String> = persistentListOf(),
+    val ignoreItems: ImmutableList<BackupIgnoreItem> = persistentListOf(),
+)
+
+@Stable
+data class BackupIgnoreItem(
+    val key: String,
+    val title: String,
+    val checked: Boolean,
+)
+
+sealed interface BackupConfigSheet {
+    data object ChooseBackupPath : BackupConfigSheet
+    data object ChooseBackupAndRun : BackupConfigSheet
+    data object BackupOptions : BackupConfigSheet
+    data object RestoreOptions : BackupConfigSheet
+    data object RestoreFiles : BackupConfigSheet
+}
+
+sealed interface BackupConfigDialog {
+    data class WebDavAuth(
+        val account: String,
+        val password: String,
+        val passwordVisible: Boolean = false,
+    ) : BackupConfigDialog
+
+    data object IgnoreRestoreItems : BackupConfigDialog
+    data class ConfirmLocalRestoreFallback(val error: String?) : BackupConfigDialog
+    data class Loading(@StringRes val titleRes: Int) : BackupConfigDialog
+}
+
+sealed interface BackupConfigIntent {
+    data class SetWebDavUrl(val value: String) : BackupConfigIntent
+    data class SetWebDavDir(val value: String) : BackupConfigIntent
+    data class SetWebDavDeviceName(val value: String) : BackupConfigIntent
+    data class SetSyncBookProgress(val value: Boolean) : BackupConfigIntent
+    data class SetSyncBookProgressPlus(val value: Boolean) : BackupConfigIntent
+    data class SetAutoCheckNewBackup(val value: Boolean) : BackupConfigIntent
+    data class SetOnlyLatestBackup(val value: Boolean) : BackupConfigIntent
+    data class SetBackupSyncMode(val value: String) : BackupConfigIntent
+    data class OpenSheet(val sheet: BackupConfigSheet) : BackupConfigIntent
+    data object DismissSheet : BackupConfigIntent
+    data object OpenWebDavAuth : BackupConfigIntent
+    data class EditWebDavAccount(val value: String) : BackupConfigIntent
+    data class EditWebDavPassword(val value: String) : BackupConfigIntent
+    data object TogglePasswordVisibility : BackupConfigIntent
+    data object SaveWebDavAuth : BackupConfigIntent
+    data object TestWebDav : BackupConfigIntent
+    data object OpenIgnoreDialog : BackupConfigIntent
+    data class ToggleIgnoreItem(val key: String, val value: Boolean) : BackupConfigIntent
+    data object SaveIgnoreItems : BackupConfigIntent
+    data object DismissDialog : BackupConfigIntent
+    data object SelectBackupDirectory : BackupConfigIntent
+    data object SelectBackupAndRunDirectory : BackupConfigIntent
+    data class BackupDirectorySelected(val path: String, val runBackup: Boolean) : BackupConfigIntent
+    data class RequestBackup(val mode: String) : BackupConfigIntent
+    data class PerformBackup(val path: String, val mode: String) : BackupConfigIntent
+    data object RequestLocalRestore : BackupConfigIntent
+    data class RestoreLocal(val uri: String) : BackupConfigIntent
+    data object RequestNetworkRestore : BackupConfigIntent
+    data class RestoreNetwork(val name: String) : BackupConfigIntent
+    data object ConfirmLocalRestoreFallback : BackupConfigIntent
+    data object RequestImportOldData : BackupConfigIntent
+}
+
+sealed interface BackupConfigEffect {
+    data object LaunchBackupDirectoryPicker : BackupConfigEffect
+    data object LaunchBackupAndRunDirectoryPicker : BackupConfigEffect
+    data object LaunchRestoreFilePicker : BackupConfigEffect
+    data object LaunchImportOldDataPicker : BackupConfigEffect
+    data class RequestStoragePermission(val path: String, val mode: String) : BackupConfigEffect
+    data class ShowMessage(@StringRes val messageRes: Int, val argument: String? = null) : BackupConfigEffect
+}

+ 255 - 51
app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt

@@ -4,100 +4,304 @@ import android.net.Uri
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import androidx.lifecycle.viewModelScope
 import io.legado.app.R
 import io.legado.app.R
+import io.legado.app.domain.gateway.BackupSettingsGateway
+import io.legado.app.domain.gateway.BackupSettingsUpdate
 import io.legado.app.domain.usecase.BackupRestoreUseCase
 import io.legado.app.domain.usecase.BackupRestoreUseCase
 import io.legado.app.domain.usecase.WebDavBackupUseCase
 import io.legado.app.domain.usecase.WebDavBackupUseCase
+import io.legado.app.utils.isContentScheme
+import kotlinx.collections.immutable.toImmutableList
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.drop
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
-import splitties.init.appCtx
 
 
 class BackupConfigViewModel(
 class BackupConfigViewModel(
+    private val settingsGateway: BackupSettingsGateway,
     private val webDavBackupUseCase: WebDavBackupUseCase,
     private val webDavBackupUseCase: WebDavBackupUseCase,
     private val backupRestoreUseCase: BackupRestoreUseCase,
     private val backupRestoreUseCase: BackupRestoreUseCase,
 ) : ViewModel() {
 ) : ViewModel() {
+    private val _uiState = MutableStateFlow(
+        BackupConfigUiState(ignoreItems = loadIgnoreItems())
+    )
+    val uiState = _uiState.asStateFlow()
 
 
-    private suspend fun syncWebDavConfig() {
-        withContext(Dispatchers.IO) {
-            webDavBackupUseCase.refreshConfig()
+    private val _effects = MutableSharedFlow<BackupConfigEffect>(extraBufferCapacity = 16)
+    val effects = _effects.asSharedFlow()
+
+    init {
+        viewModelScope.launch {
+            settingsGateway.settings.collect { settings ->
+                _uiState.update { it.copy(settings = settings) }
+            }
+        }
+        viewModelScope.launch {
+            settingsGateway.settings
+                .map { listOf(it.webDavUrl, it.webDavDir, it.webDavAccount, it.webDavPassword) }
+                .distinctUntilChanged()
+                .drop(1)
+                .collect { withContext(Dispatchers.IO) { webDavBackupUseCase.refreshConfig() } }
+        }
+    }
+
+    fun onIntent(intent: BackupConfigIntent) {
+        when (intent) {
+            is BackupConfigIntent.SetWebDavUrl -> update(BackupSettingsUpdate.WebDavUrl(intent.value))
+            is BackupConfigIntent.SetWebDavDir -> update(BackupSettingsUpdate.WebDavDir(intent.value))
+            is BackupConfigIntent.SetWebDavDeviceName ->
+                update(BackupSettingsUpdate.WebDavDeviceName(intent.value))
+            is BackupConfigIntent.SetSyncBookProgress -> setSyncBookProgress(intent.value)
+            is BackupConfigIntent.SetSyncBookProgressPlus ->
+                update(BackupSettingsUpdate.SyncBookProgressPlus(intent.value))
+            is BackupConfigIntent.SetAutoCheckNewBackup ->
+                update(BackupSettingsUpdate.AutoCheckNewBackup(intent.value))
+            is BackupConfigIntent.SetOnlyLatestBackup ->
+                update(BackupSettingsUpdate.OnlyLatestBackup(intent.value))
+            is BackupConfigIntent.SetBackupSyncMode ->
+                update(BackupSettingsUpdate.BackupSyncMode(intent.value))
+            is BackupConfigIntent.OpenSheet -> _uiState.update { it.copy(activeSheet = intent.sheet) }
+            BackupConfigIntent.DismissSheet -> _uiState.update { it.copy(activeSheet = null) }
+            BackupConfigIntent.OpenWebDavAuth -> openWebDavAuth()
+            is BackupConfigIntent.EditWebDavAccount -> updateAuth { it.copy(account = intent.value) }
+            is BackupConfigIntent.EditWebDavPassword -> updateAuth { it.copy(password = intent.value) }
+            BackupConfigIntent.TogglePasswordVisibility ->
+                updateAuth { it.copy(passwordVisible = !it.passwordVisible) }
+            BackupConfigIntent.SaveWebDavAuth -> saveWebDavAuth()
+            BackupConfigIntent.TestWebDav -> testWebDav()
+            BackupConfigIntent.OpenIgnoreDialog ->
+                _uiState.update { it.copy(activeDialog = BackupConfigDialog.IgnoreRestoreItems) }
+            is BackupConfigIntent.ToggleIgnoreItem -> toggleIgnoreItem(intent.key, intent.value)
+            BackupConfigIntent.SaveIgnoreItems -> saveIgnoreItems()
+            BackupConfigIntent.DismissDialog -> _uiState.update { it.copy(activeDialog = null) }
+            BackupConfigIntent.SelectBackupDirectory -> launchDirectoryPicker(runBackup = false)
+            BackupConfigIntent.SelectBackupAndRunDirectory -> launchDirectoryPicker(runBackup = true)
+            is BackupConfigIntent.BackupDirectorySelected -> saveBackupPath(intent.path, intent.runBackup)
+            is BackupConfigIntent.RequestBackup -> requestBackup(intent.mode)
+            is BackupConfigIntent.PerformBackup -> performBackup(intent.path, intent.mode)
+            BackupConfigIntent.RequestLocalRestore -> requestLocalRestore()
+            is BackupConfigIntent.RestoreLocal -> restoreLocal(intent.uri)
+            BackupConfigIntent.RequestNetworkRestore -> loadNetworkBackups()
+            is BackupConfigIntent.RestoreNetwork -> restoreNetwork(intent.name)
+            BackupConfigIntent.ConfirmLocalRestoreFallback -> requestLocalRestore()
+            BackupConfigIntent.RequestImportOldData ->
+                _effects.tryEmit(BackupConfigEffect.LaunchImportOldDataPicker)
         }
         }
     }
     }
 
 
-    suspend fun refreshWebDavConfig() {
-        syncWebDavConfig()
+    private fun update(update: BackupSettingsUpdate) {
+        viewModelScope.launch { settingsGateway.update(update) }
     }
     }
 
 
-    fun setWebDavAccount(account: String, password: String) {
-        BackupConfig.webDavAccount = account
-        BackupConfig.webDavPassword = password
+    private fun setSyncBookProgress(value: Boolean) {
+        viewModelScope.launch {
+            val updates = buildList {
+                add(BackupSettingsUpdate.SyncBookProgress(value))
+                if (!value) add(BackupSettingsUpdate.SyncBookProgressPlus(false))
+            }
+            settingsGateway.updateAll(updates)
+        }
+    }
+
+    private fun openWebDavAuth() {
+        val settings = _uiState.value.settings
+        _uiState.update {
+            it.copy(
+                activeDialog = BackupConfigDialog.WebDavAuth(
+                    account = settings.webDavAccount,
+                    password = settings.webDavPassword,
+                )
+            )
+        }
     }
     }
 
 
-    fun getWebDavAccount(): String {
-        return BackupConfig.webDavAccount
+    private fun updateAuth(transform: (BackupConfigDialog.WebDavAuth) -> BackupConfigDialog.WebDavAuth) {
+        _uiState.update { state ->
+            val dialog = state.activeDialog as? BackupConfigDialog.WebDavAuth ?: return@update state
+            state.copy(activeDialog = transform(dialog))
+        }
     }
     }
 
 
-    fun getWebDavPassword(): String {
-        return BackupConfig.webDavPassword
+    private fun saveWebDavAuth() {
+        val dialog = _uiState.value.activeDialog as? BackupConfigDialog.WebDavAuth ?: return
+        viewModelScope.launch {
+            settingsGateway.update(
+                BackupSettingsUpdate.WebDavCredentials(dialog.account, dialog.password)
+            )
+            testWebDav()
+        }
     }
     }
 
 
-    suspend fun testWebDav(): Boolean {
-        return withContext(Dispatchers.IO) {
-            try {
-                webDavBackupUseCase.test()
-            } catch (e: Exception) {
-                false
+    private fun testWebDav() {
+        _uiState.update { it.copy(activeDialog = BackupConfigDialog.Loading(R.string.test_sync_loading_text)) }
+        viewModelScope.launch {
+            val success = withContext(Dispatchers.IO) {
+                runCatching { webDavBackupUseCase.test() }.getOrDefault(false)
             }
             }
+            _uiState.update { it.copy(activeDialog = null) }
+            _effects.tryEmit(
+                BackupConfigEffect.ShowMessage(
+                    if (success) R.string.test_sync_status_success else R.string.test_sync_status_fail
+                )
+            )
+        }
+    }
+
+    private fun toggleIgnoreItem(key: String, value: Boolean) {
+        _uiState.update { state ->
+            state.copy(
+                ignoreItems = state.ignoreItems.map { item ->
+                    if (item.key == key) item.copy(checked = value) else item
+                }.toImmutableList()
+            )
         }
         }
     }
     }
 
 
-    fun backup(backupPath: String, mode: String = "both", onSuccess: () -> Unit, onError: (String) -> Unit) {
+    private fun saveIgnoreItems() {
+        _uiState.value.ignoreItems.forEach { item ->
+            io.legado.app.help.storage.BackupConfig.ignoreConfig[item.key] = item.checked
+        }
+        io.legado.app.help.storage.BackupConfig.saveIgnoreConfig()
+        _uiState.update { it.copy(activeDialog = null) }
+    }
+
+    private fun launchDirectoryPicker(runBackup: Boolean) {
+        _uiState.update { it.copy(activeSheet = null) }
+        _effects.tryEmit(
+            if (runBackup) BackupConfigEffect.LaunchBackupAndRunDirectoryPicker
+            else BackupConfigEffect.LaunchBackupDirectoryPicker
+        )
+    }
+
+    private fun saveBackupPath(path: String, runBackup: Boolean) {
+        viewModelScope.launch {
+            settingsGateway.update(BackupSettingsUpdate.BackupPath(path))
+            if (runBackup && path.isNotEmpty()) requestBackup("both", path)
+        }
+    }
+
+    private fun requestBackup(mode: String, selectedPath: String? = null) {
+        _uiState.update { it.copy(activeSheet = null) }
+        val path = selectedPath ?: _uiState.value.settings.backupPath.orEmpty()
+        if (path.isEmpty() && mode != "webdav") return
+        if (path.isNotEmpty() && !path.isContentScheme()) {
+            _effects.tryEmit(BackupConfigEffect.RequestStoragePermission(path, mode))
+        } else {
+            performBackup(path, mode)
+        }
+    }
+
+    private fun performBackup(path: String, mode: String) {
+        _uiState.update { it.copy(activeDialog = BackupConfigDialog.Loading(R.string.backup)) }
         viewModelScope.launch(Dispatchers.IO) {
         viewModelScope.launch(Dispatchers.IO) {
-            try {
-                backupRestoreUseCase.backup(backupPath, mode)
-                withContext(Dispatchers.Main) {
-                    onSuccess()
+            runCatching { backupRestoreUseCase.backup(path, mode) }
+                .onSuccess {
+                    withContext(Dispatchers.Main) {
+                        _uiState.update { it.copy(activeDialog = null) }
+                        _effects.tryEmit(BackupConfigEffect.ShowMessage(R.string.backup_success))
+                    }
                 }
                 }
-            } catch (e: Exception) {
-                withContext(Dispatchers.Main) {
-                    onError(e.localizedMessage ?: appCtx.getString(R.string.backup_error))
+                .onFailure { error ->
+                    withContext(Dispatchers.Main) {
+                        _uiState.update { it.copy(activeDialog = null) }
+                        _effects.tryEmit(
+                            BackupConfigEffect.ShowMessage(R.string.backup_fail, error.localizedMessage)
+                        )
+                    }
                 }
                 }
-            }
         }
         }
     }
     }
 
 
-    suspend fun getBackupNames(): List<String> {
-        return withContext(Dispatchers.IO) {
-            webDavBackupUseCase.getBackupNames()
+    private fun requestLocalRestore() {
+        _uiState.update { it.copy(activeSheet = null, activeDialog = null) }
+        _effects.tryEmit(BackupConfigEffect.LaunchRestoreFilePicker)
+    }
+
+    private fun restoreLocal(uri: String) {
+        _uiState.update { it.copy(activeDialog = BackupConfigDialog.Loading(R.string.on_restore)) }
+        viewModelScope.launch(Dispatchers.IO) {
+            runCatching { backupRestoreUseCase.restoreLocal(Uri.parse(uri).toString()) }
+                .fold(
+                    onSuccess = { finishRestoreSuccess() },
+                    onFailure = { finishRestoreFailure(it.localizedMessage) },
+                )
         }
         }
     }
     }
 
 
-    fun restoreWebDav(name: String, onSuccess: () -> Unit, onError: (String) -> Unit) {
+    private fun loadNetworkBackups() {
+        _uiState.update {
+            it.copy(activeSheet = null, activeDialog = BackupConfigDialog.Loading(R.string.loading))
+        }
         viewModelScope.launch(Dispatchers.IO) {
         viewModelScope.launch(Dispatchers.IO) {
-            try {
-                webDavBackupUseCase.restore(name)
-                withContext(Dispatchers.Main) {
-                    onSuccess()
+            runCatching { webDavBackupUseCase.getBackupNames() }
+                .onSuccess { names ->
+                    withContext(Dispatchers.Main) {
+                        _uiState.update {
+                            it.copy(
+                                activeDialog = null,
+                                activeSheet = BackupConfigSheet.RestoreFiles,
+                                backupNames = names.toImmutableList(),
+                            )
+                        }
+                    }
                 }
                 }
-            } catch (e: Exception) {
-                withContext(Dispatchers.Main) {
-                    onError(e.localizedMessage ?: appCtx.getString(R.string.restore_error))
+                .onFailure { error ->
+                    withContext(Dispatchers.Main) {
+                        _uiState.update {
+                            it.copy(
+                                activeDialog = BackupConfigDialog.ConfirmLocalRestoreFallback(
+                                    error.localizedMessage
+                                )
+                            )
+                        }
+                    }
                 }
                 }
-            }
         }
         }
     }
     }
 
 
-    fun restore(uri: Uri, onSuccess: () -> Unit, onError: (String) -> Unit) {
+    private fun restoreNetwork(name: String) {
+        _uiState.update {
+            it.copy(activeSheet = null, activeDialog = BackupConfigDialog.Loading(R.string.on_restore))
+        }
         viewModelScope.launch(Dispatchers.IO) {
         viewModelScope.launch(Dispatchers.IO) {
-            try {
-                backupRestoreUseCase.restoreLocal(uri.toString())
-                withContext(Dispatchers.Main) {
-                    onSuccess()
-                }
-            } catch (e: Exception) {
-                withContext(Dispatchers.Main) {
-                    onError(e.localizedMessage ?: appCtx.getString(R.string.restore_error))
-                }
-            }
+            runCatching { webDavBackupUseCase.restore(name) }
+                .fold(
+                    onSuccess = { finishRestoreSuccess() },
+                    onFailure = { finishRestoreFailure(it.localizedMessage, webDav = true) },
+                )
         }
         }
     }
     }
 
 
+    private suspend fun finishRestoreSuccess() = withContext(Dispatchers.Main) {
+        _uiState.update { it.copy(activeDialog = null) }
+        _effects.tryEmit(BackupConfigEffect.ShowMessage(R.string.restore_success))
+    }
+
+    private suspend fun finishRestoreFailure(message: String?, webDav: Boolean = false) =
+        withContext(Dispatchers.Main) {
+            _uiState.update { it.copy(activeDialog = null) }
+            _effects.tryEmit(
+                BackupConfigEffect.ShowMessage(
+                    if (webDav) R.string.webdav_restore_fail else R.string.restore_fail_with_error,
+                    message,
+                )
+            )
+        }
+
+    private companion object {
+        fun loadIgnoreItems() = io.legado.app.help.storage.BackupConfig.ignoreKeys
+            .mapIndexed { index, key ->
+                BackupIgnoreItem(
+                    key = key,
+                    title = io.legado.app.help.storage.BackupConfig.ignoreTitle[index],
+                    checked = io.legado.app.help.storage.BackupConfig.ignoreConfig[key] ?: false,
+                )
+            }
+            .toImmutableList()
+    }
 }
 }

+ 49 - 0
app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigContract.kt

@@ -0,0 +1,49 @@
+package io.legado.app.ui.config.coverConfig
+
+import androidx.compose.runtime.Stable
+import io.legado.app.domain.gateway.CoverSettingsUpdate
+import io.legado.app.domain.model.settings.CoverSettings
+
+@Stable
+data class CoverConfigUiState(
+    val settings: CoverSettings = CoverSettings(),
+    val albumSelection: CoverAlbumSelectionUiState = CoverAlbumSelectionUiState(),
+    val activeSheet: CoverConfigSheet? = null,
+    val rule: CoverRuleUiState = CoverRuleUiState(),
+)
+
+@Stable
+data class CoverRuleUiState(
+    val enabled: Boolean = false,
+    val searchUrl: String = "",
+    val coverRule: String = "",
+)
+
+sealed interface CoverConfigSheet {
+    data object Rule : CoverConfigSheet
+    data object Album : CoverConfigSheet
+    data class Color(val field: CoverColorField) : CoverConfigSheet
+}
+
+enum class CoverColorField {
+    Text,
+    Shadow,
+    TextDark,
+    ShadowDark,
+}
+
+sealed interface CoverConfigIntent {
+    data class UpdateSetting(val update: CoverSettingsUpdate) : CoverConfigIntent
+    data class ShowSheet(val sheet: CoverConfigSheet) : CoverConfigIntent
+    data object DismissSheet : CoverConfigIntent
+    data class SelectAlbum(val id: String?) : CoverConfigIntent
+    data class SetRuleEnabled(val value: Boolean) : CoverConfigIntent
+    data class SetRuleSearchUrl(val value: String) : CoverConfigIntent
+    data class SetRuleExpression(val value: String) : CoverConfigIntent
+    data object RestoreDefaultRule : CoverConfigIntent
+    data object SaveRule : CoverConfigIntent
+}
+
+sealed interface CoverConfigEffect {
+    data class ShowToast(val stringRes: Int) : CoverConfigEffect
+}

+ 116 - 69
app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigScreen.kt

@@ -11,9 +11,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
@@ -33,21 +31,49 @@ import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
+import androidx.compose.ui.platform.LocalContext
+import io.legado.app.domain.gateway.CoverSettingsUpdate
+import io.legado.app.utils.toastOnUi
+import kotlinx.coroutines.flow.collectLatest
+
+@Composable
+fun CoverConfigRouteScreen(
+    onBackClick: () -> Unit,
+    onNavigateToCoverAlbums: () -> Unit,
+    viewModel: CoverConfigViewModel = koinViewModel(),
+) {
+    val context = LocalContext.current
+    val state by viewModel.uiState.collectAsStateWithLifecycle()
+    LaunchedEffect(Unit) {
+        viewModel.effects.collectLatest { effect ->
+            when (effect) {
+                is CoverConfigEffect.ShowToast -> context.toastOnUi(effect.stringRes)
+            }
+        }
+    }
+    CoverConfigScreen(
+        state = state,
+        onIntent = viewModel::onIntent,
+        onBackClick = onBackClick,
+        onNavigateToCoverAlbums = onNavigateToCoverAlbums,
+    )
+}
 
 
 @OptIn(ExperimentalMaterial3Api::class)
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 @Composable
 fun CoverConfigScreen(
 fun CoverConfigScreen(
+    state: CoverConfigUiState,
+    onIntent: (CoverConfigIntent) -> Unit,
     onBackClick: () -> Unit,
     onBackClick: () -> Unit,
     onNavigateToCoverAlbums: () -> Unit,
     onNavigateToCoverAlbums: () -> Unit,
-    viewModel: CoverConfigViewModel = koinViewModel()
 ) {
 ) {
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
-    val albumState by viewModel.albumState.collectAsStateWithLifecycle()
+    val settings = state.settings
+    val albumState = state.albumSelection
     val selectedAlbum = albumState.albums
     val selectedAlbum = albumState.albums
         .firstOrNull { it.id == albumState.selectedAlbumId }
         .firstOrNull { it.id == albumState.selectedAlbumId }
-    var showCoverRuleSheet by remember { mutableStateOf(false) }
-    var showAlbumSelect by remember { mutableStateOf(false) }
-    var showColorPickerByField by remember { mutableStateOf<String?>(null) }
+    fun update(update: CoverSettingsUpdate) =
+        onIntent(CoverConfigIntent.UpdateSetting(update))
 
 
     AppScaffold(
     AppScaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
@@ -73,21 +99,21 @@ fun CoverConfigScreen(
                 SwitchSettingItem(
                 SwitchSettingItem(
                     title = stringResource(R.string.only_wifi),
                     title = stringResource(R.string.only_wifi),
                     description = stringResource(R.string.only_wifi_summary),
                     description = stringResource(R.string.only_wifi_summary),
-                    checked = CoverConfig.loadCoverOnlyWifi,
-                    onCheckedChange = { CoverConfig.loadCoverOnlyWifi = it }
+                    checked = settings.loadOnlyOnWifi,
+                    onCheckedChange = { update(CoverSettingsUpdate.LoadOnlyOnWifi(it)) }
                 )
                 )
 
 
                 ClickableSettingItem(
                 ClickableSettingItem(
                     title = stringResource(R.string.cover_rule),
                     title = stringResource(R.string.cover_rule),
                     description = stringResource(R.string.cover_rule_summary),
                     description = stringResource(R.string.cover_rule_summary),
-                    onClick = { showCoverRuleSheet = true }
+                    onClick = { onIntent(CoverConfigIntent.ShowSheet(CoverConfigSheet.Rule)) }
                 )
                 )
 
 
                 SwitchSettingItem(
                 SwitchSettingItem(
                     title = stringResource(R.string.use_default_cover),
                     title = stringResource(R.string.use_default_cover),
                     description = stringResource(R.string.use_default_cover_s),
                     description = stringResource(R.string.use_default_cover_s),
-                    checked = CoverConfig.useDefaultCover,
-                    onCheckedChange = { CoverConfig.useDefaultCover = it }
+                    checked = settings.useDefaultCover,
+                    onCheckedChange = { update(CoverSettingsUpdate.UseDefaultCover(it)) }
                 )
                 )
 
 
                 ClickableSettingItem(
                 ClickableSettingItem(
@@ -101,33 +127,30 @@ fun CoverConfigScreen(
                             )
                             )
                         }"
                         }"
                     } ?: stringResource(R.string.cover_album_none),
                     } ?: stringResource(R.string.cover_album_none),
-                    onClick = { showAlbumSelect = true }
+                    onClick = { onIntent(CoverConfigIntent.ShowSheet(CoverConfigSheet.Album)) }
                 )
                 )
 
 
                 SwitchSettingItem(
                 SwitchSettingItem(
                     title = stringResource(R.string.cover_show_shadow),
                     title = stringResource(R.string.cover_show_shadow),
-                    checked = CoverConfig.coverShowShadow,
+                    checked = settings.showShadow,
                     onCheckedChange = {
                     onCheckedChange = {
-                        CoverConfig.coverShowShadow = it
-                        viewModel.updateCoverStyle()
+                        update(CoverSettingsUpdate.ShowShadow(it))
                     }
                     }
                 )
                 )
 
 
                 SwitchSettingItem(
                 SwitchSettingItem(
                     title = stringResource(R.string.cover_show_stroke),
                     title = stringResource(R.string.cover_show_stroke),
-                    checked = CoverConfig.coverShowStroke,
+                    checked = settings.showStroke,
                     onCheckedChange = {
                     onCheckedChange = {
-                        CoverConfig.coverShowStroke = it
-                        viewModel.updateCoverStyle()
+                        update(CoverSettingsUpdate.ShowStroke(it))
                     }
                     }
                 )
                 )
 
 
                 SwitchSettingItem(
                 SwitchSettingItem(
                     title = stringResource(R.string.default_color),
                     title = stringResource(R.string.default_color),
-                    checked = CoverConfig.coverDefaultColor,
+                    checked = settings.useDefaultColor,
                     onCheckedChange = {
                     onCheckedChange = {
-                        CoverConfig.coverDefaultColor = it
-                        viewModel.updateCoverStyle()
+                        update(CoverSettingsUpdate.UseDefaultColor(it))
                     }
                     }
                 )
                 )
             }
             }
@@ -135,15 +158,14 @@ fun CoverConfigScreen(
             SplicedColumnGroup {
             SplicedColumnGroup {
                 DropdownListSettingItem(
                 DropdownListSettingItem(
                     title = stringResource(R.string.cover_info_orientation),
                     title = stringResource(R.string.cover_info_orientation),
-                    selectedValue = CoverConfig.coverInfoOrientation,
+                    selectedValue = settings.infoOrientation,
                     displayEntries = arrayOf(
                     displayEntries = arrayOf(
                         stringResource(R.string.screen_portrait),
                         stringResource(R.string.screen_portrait),
                         stringResource(R.string.screen_landscape)
                         stringResource(R.string.screen_landscape)
                     ),
                     ),
                     entryValues = arrayOf("0", "1"),
                     entryValues = arrayOf("0", "1"),
                     onValueChange = {
                     onValueChange = {
-                        CoverConfig.coverInfoOrientation = it
-                        viewModel.updateCoverStyle()
+                        update(CoverSettingsUpdate.InfoOrientation(it))
                     }
                     }
                 )
                 )
             }
             }
@@ -151,7 +173,7 @@ fun CoverConfigScreen(
             SplicedColumnGroup(title = stringResource(R.string.network_book_badge_setting)) {
             SplicedColumnGroup(title = stringResource(R.string.network_book_badge_setting)) {
                 DropdownListSettingItem(
                 DropdownListSettingItem(
                     title = stringResource(R.string.network_book_badge_setting),
                     title = stringResource(R.string.network_book_badge_setting),
-                    selectedValue = CoverConfig.exploreFilterState.toString(),
+                    selectedValue = settings.exploreFilterState.toString(),
                     displayEntries = arrayOf(
                     displayEntries = arrayOf(
                         stringResource(R.string.filter_show_all),
                         stringResource(R.string.filter_show_all),
                         stringResource(R.string.filter_hide_in_shelf),
                         stringResource(R.string.filter_hide_in_shelf),
@@ -160,7 +182,7 @@ fun CoverConfigScreen(
                     ),
                     ),
                     entryValues = arrayOf("0", "1", "2", "3"),
                     entryValues = arrayOf("0", "1", "2", "3"),
                     onValueChange = {
                     onValueChange = {
-                        CoverConfig.exploreFilterState = it.toInt()
+                        update(CoverSettingsUpdate.ExploreFilterState(it.toInt()))
                     }
                     }
                 )
                 )
             }
             }
@@ -168,14 +190,20 @@ fun CoverConfigScreen(
             SplicedColumnGroup(title = stringResource(R.string.day)) {
             SplicedColumnGroup(title = stringResource(R.string.day)) {
                 ClickableSettingItem(
                 ClickableSettingItem(
                     title = stringResource(R.string.text_color),
                     title = stringResource(R.string.text_color),
-                    option = "#${Integer.toHexString(CoverConfig.coverTextColor).uppercase()}",
-                    onClick = { showColorPickerByField = "coverTextColor" },
+                    option = "#${Integer.toHexString(settings.textColor).uppercase()}",
+                    onClick = {
+                        onIntent(
+                            CoverConfigIntent.ShowSheet(
+                                CoverConfigSheet.Color(CoverColorField.Text)
+                            )
+                        )
+                    },
                     trailingContent = {
                     trailingContent = {
                         Box(
                         Box(
                             modifier = Modifier
                             modifier = Modifier
                                 .size(28.dp)
                                 .size(28.dp)
                                 .clip(CircleShape)
                                 .clip(CircleShape)
-                                .background(Color(CoverConfig.coverTextColor))
+                                .background(Color(settings.textColor))
                                 .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
                                 .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
                         )
                         )
                     }
                     }
@@ -183,14 +211,20 @@ fun CoverConfigScreen(
 
 
                 ClickableSettingItem(
                 ClickableSettingItem(
                     title = stringResource(R.string.text_shadow_color),
                     title = stringResource(R.string.text_shadow_color),
-                    option = "#${Integer.toHexString(CoverConfig.coverShadowColor).uppercase()}",
-                    onClick = { showColorPickerByField = "coverShadowColor" },
+                    option = "#${Integer.toHexString(settings.shadowColor).uppercase()}",
+                    onClick = {
+                        onIntent(
+                            CoverConfigIntent.ShowSheet(
+                                CoverConfigSheet.Color(CoverColorField.Shadow)
+                            )
+                        )
+                    },
                     trailingContent = {
                     trailingContent = {
                         Box(
                         Box(
                             modifier = Modifier
                             modifier = Modifier
                                 .size(28.dp)
                                 .size(28.dp)
                                 .clip(CircleShape)
                                 .clip(CircleShape)
-                                .background(Color(CoverConfig.coverShadowColor))
+                                .background(Color(settings.shadowColor))
                                 .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
                                 .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
                         )
                         )
                     }
                     }
@@ -199,30 +233,36 @@ fun CoverConfigScreen(
                 SwitchSettingItem(
                 SwitchSettingItem(
                     title = stringResource(R.string.cover_show_name),
                     title = stringResource(R.string.cover_show_name),
                     description = stringResource(R.string.cover_show_name_summary),
                     description = stringResource(R.string.cover_show_name_summary),
-                    checked = CoverConfig.coverShowName,
-                    onCheckedChange = { viewModel.updateShowName(it) }
+                    checked = settings.showName,
+                    onCheckedChange = { update(CoverSettingsUpdate.ShowName(it, false)) }
                 )
                 )
 
 
                 SwitchSettingItem(
                 SwitchSettingItem(
                     title = stringResource(R.string.cover_show_author),
                     title = stringResource(R.string.cover_show_author),
                     description = stringResource(R.string.cover_show_author_summary),
                     description = stringResource(R.string.cover_show_author_summary),
-                    checked = CoverConfig.coverShowAuthor,
-                    enabled = CoverConfig.coverShowName,
-                    onCheckedChange = { viewModel.updateShowAuthor(it) }
+                    checked = settings.showAuthor,
+                    enabled = settings.showName,
+                    onCheckedChange = { update(CoverSettingsUpdate.ShowAuthor(it, false)) }
                 )
                 )
             }
             }
 
 
             SplicedColumnGroup(title = stringResource(R.string.night)) {
             SplicedColumnGroup(title = stringResource(R.string.night)) {
                 ClickableSettingItem(
                 ClickableSettingItem(
                     title = stringResource(R.string.text_color),
                     title = stringResource(R.string.text_color),
-                    option = "#${Integer.toHexString(CoverConfig.coverTextColorN).uppercase()}",
-                    onClick = { showColorPickerByField = "coverTextColorN" },
+                    option = "#${Integer.toHexString(settings.textColorDark).uppercase()}",
+                    onClick = {
+                        onIntent(
+                            CoverConfigIntent.ShowSheet(
+                                CoverConfigSheet.Color(CoverColorField.TextDark)
+                            )
+                        )
+                    },
                     trailingContent = {
                     trailingContent = {
                         Box(
                         Box(
                             modifier = Modifier
                             modifier = Modifier
                                 .size(28.dp)
                                 .size(28.dp)
                                 .clip(CircleShape)
                                 .clip(CircleShape)
-                                .background(Color(CoverConfig.coverTextColorN))
+                                .background(Color(settings.textColorDark))
                                 .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
                                 .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
                         )
                         )
                     }
                     }
@@ -230,14 +270,20 @@ fun CoverConfigScreen(
 
 
                 ClickableSettingItem(
                 ClickableSettingItem(
                     title = stringResource(R.string.text_shadow_color),
                     title = stringResource(R.string.text_shadow_color),
-                    option = "#${Integer.toHexString(CoverConfig.coverShadowColorN).uppercase()}",
-                    onClick = { showColorPickerByField = "coverShadowColorN" },
+                    option = "#${Integer.toHexString(settings.shadowColorDark).uppercase()}",
+                    onClick = {
+                        onIntent(
+                            CoverConfigIntent.ShowSheet(
+                                CoverConfigSheet.Color(CoverColorField.ShadowDark)
+                            )
+                        )
+                    },
                     trailingContent = {
                     trailingContent = {
                         Box(
                         Box(
                             modifier = Modifier
                             modifier = Modifier
                                 .size(28.dp)
                                 .size(28.dp)
                                 .clip(CircleShape)
                                 .clip(CircleShape)
-                                .background(Color(CoverConfig.coverShadowColorN))
+                                .background(Color(settings.shadowColorDark))
                                 .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
                                 .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
                         )
                         )
                     }
                     }
@@ -246,16 +292,16 @@ fun CoverConfigScreen(
                 SwitchSettingItem(
                 SwitchSettingItem(
                     title = stringResource(R.string.cover_show_name),
                     title = stringResource(R.string.cover_show_name),
                     description = stringResource(R.string.cover_show_name_summary),
                     description = stringResource(R.string.cover_show_name_summary),
-                    checked = CoverConfig.coverShowNameN,
-                    onCheckedChange = { viewModel.updateShowName(it, true) }
+                    checked = settings.showNameDark,
+                    onCheckedChange = { update(CoverSettingsUpdate.ShowName(it, true)) }
                 )
                 )
 
 
                 SwitchSettingItem(
                 SwitchSettingItem(
                     title = stringResource(R.string.cover_show_author),
                     title = stringResource(R.string.cover_show_author),
                     description = stringResource(R.string.cover_show_author_summary),
                     description = stringResource(R.string.cover_show_author_summary),
-                    checked = CoverConfig.coverShowAuthorN,
-                    enabled = CoverConfig.coverShowNameN,
-                    onCheckedChange = { viewModel.updateShowAuthor(it, true) }
+                    checked = settings.showAuthorDark,
+                    enabled = settings.showNameDark,
+                    onCheckedChange = { update(CoverSettingsUpdate.ShowAuthor(it, true)) }
                 )
                 )
                 }
                 }
             }
             }
@@ -263,44 +309,45 @@ fun CoverConfigScreen(
     }
     }
 
 
     CoverRuleConfigSheet(
     CoverRuleConfigSheet(
-        show = showCoverRuleSheet,
-        onDismissRequest = { showCoverRuleSheet = false }
+        show = state.activeSheet == CoverConfigSheet.Rule,
+        state = state.rule,
+        onIntent = onIntent,
+        onDismissRequest = { onIntent(CoverConfigIntent.DismissSheet) },
     )
     )
 
 
-    if (showAlbumSelect) {
+    if (state.activeSheet == CoverConfigSheet.Album) {
         CoverAlbumSelectSheet(
         CoverAlbumSelectSheet(
             show = true,
             show = true,
             state = albumState,
             state = albumState,
-            onSelect = viewModel::selectAlbum,
+            onSelect = { onIntent(CoverConfigIntent.SelectAlbum(it)) },
             onManage = {
             onManage = {
-                showAlbumSelect = false
+                onIntent(CoverConfigIntent.DismissSheet)
                 onNavigateToCoverAlbums()
                 onNavigateToCoverAlbums()
             },
             },
-            onDismissRequest = { showAlbumSelect = false },
+            onDismissRequest = { onIntent(CoverConfigIntent.DismissSheet) },
         )
         )
     }
     }
 
 
-    showColorPickerByField?.let { field ->
+    (state.activeSheet as? CoverConfigSheet.Color)?.field?.let { field ->
         val initialColor = when (field) {
         val initialColor = when (field) {
-            "coverTextColor" -> CoverConfig.coverTextColor
-            "coverShadowColor" -> CoverConfig.coverShadowColor
-            "coverTextColorN" -> CoverConfig.coverTextColorN
-            "coverShadowColorN" -> CoverConfig.coverShadowColorN
-            else -> 0
+            CoverColorField.Text -> settings.textColor
+            CoverColorField.Shadow -> settings.shadowColor
+            CoverColorField.TextDark -> settings.textColorDark
+            CoverColorField.ShadowDark -> settings.shadowColorDark
         }
         }
 
 
         ColorPickerSheet(
         ColorPickerSheet(
-            show = showColorPickerByField != null,
+            show = true,
             initialColor = initialColor,
             initialColor = initialColor,
-            onDismissRequest = { showColorPickerByField = null },
+            onDismissRequest = { onIntent(CoverConfigIntent.DismissSheet) },
             onColorSelected = { color ->
             onColorSelected = { color ->
                 when (field) {
                 when (field) {
-                    "coverTextColor" -> CoverConfig.coverTextColor = color
-                    "coverShadowColor" -> CoverConfig.coverShadowColor = color
-                    "coverTextColorN" -> CoverConfig.coverTextColorN = color
-                    "coverShadowColorN" -> CoverConfig.coverShadowColorN = color
+                    CoverColorField.Text -> update(CoverSettingsUpdate.TextColor(color, false))
+                    CoverColorField.Shadow -> update(CoverSettingsUpdate.ShadowColor(color, false))
+                    CoverColorField.TextDark -> update(CoverSettingsUpdate.TextColor(color, true))
+                    CoverColorField.ShadowDark -> update(CoverSettingsUpdate.ShadowColor(color, true))
                 }
                 }
-                viewModel.updateCoverStyle()
+                onIntent(CoverConfigIntent.DismissSheet)
             }
             }
         )
         )
     }
     }

+ 80 - 38
app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigViewModel.kt

@@ -2,56 +2,104 @@ package io.legado.app.ui.config.coverConfig
 
 
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import androidx.lifecycle.viewModelScope
+import io.legado.app.R
+import io.legado.app.domain.gateway.CoverSettingsGateway
 import io.legado.app.domain.usecase.CoverAlbumUseCase
 import io.legado.app.domain.usecase.CoverAlbumUseCase
+import io.legado.app.help.DefaultData
+import io.legado.app.model.BookCover
 import kotlinx.collections.immutable.toImmutableList
 import kotlinx.collections.immutable.toImmutableList
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 
 
 class CoverConfigViewModel(
 class CoverConfigViewModel(
     private val coverAlbumUseCase: CoverAlbumUseCase,
     private val coverAlbumUseCase: CoverAlbumUseCase,
+    private val settingsGateway: CoverSettingsGateway,
 ) : ViewModel() {
 ) : ViewModel() {
 
 
-    val albumState = combine(
-        coverAlbumUseCase.albums,
-        coverAlbumUseCase.selection,
-    ) { albums, selection ->
-        CoverAlbumSelectionUiState(
-            albums = albums.map { it.toUi() }.toImmutableList(),
-            selectedAlbumId = selection.albumId,
-        )
-    }.stateIn(
-        scope = viewModelScope,
-        started = SharingStarted.WhileSubscribed(5_000),
-        initialValue = CoverAlbumSelectionUiState(),
-    )
+    private val _uiState = MutableStateFlow(CoverConfigUiState())
+    val uiState = _uiState.asStateFlow()
 
 
-    fun selectAlbum(albumId: String?) {
-        viewModelScope.launch(Dispatchers.IO) {
-            coverAlbumUseCase.selectAlbum(albumId)
+    private val _effects = MutableSharedFlow<CoverConfigEffect>(extraBufferCapacity = 16)
+    val effects = _effects.asSharedFlow()
+
+    init {
+        viewModelScope.launch {
+            combine(
+                coverAlbumUseCase.albums,
+                coverAlbumUseCase.selection,
+                settingsGateway.settings,
+            ) { albums, selection, settings ->
+                settings to CoverAlbumSelectionUiState(
+                    albums = albums.map { it.toUi() }.toImmutableList(),
+                    selectedAlbumId = selection.albumId,
+                )
+            }.collect { (settings, albumSelection) ->
+                _uiState.update {
+                    it.copy(settings = settings, albumSelection = albumSelection)
+                }
+            }
         }
         }
     }
     }
 
 
-    fun updateShowName(show: Boolean, isNight: Boolean = false) {
-        if (isNight) {
-            CoverConfig.coverShowNameN = show
-        } else {
-            CoverConfig.coverShowName = show
+    fun onIntent(intent: CoverConfigIntent) {
+        when (intent) {
+            is CoverConfigIntent.UpdateSetting -> viewModelScope.launch {
+                settingsGateway.update(intent.update)
+            }
+            is CoverConfigIntent.ShowSheet -> {
+                _uiState.update { it.copy(activeSheet = intent.sheet) }
+                if (intent.sheet == CoverConfigSheet.Rule) loadRule()
+            }
+            CoverConfigIntent.DismissSheet ->
+                _uiState.update { it.copy(activeSheet = null) }
+            is CoverConfigIntent.SelectAlbum -> viewModelScope.launch(Dispatchers.IO) {
+                coverAlbumUseCase.selectAlbum(intent.id)
+            }
+            is CoverConfigIntent.SetRuleEnabled ->
+                _uiState.update { it.copy(rule = it.rule.copy(enabled = intent.value)) }
+            is CoverConfigIntent.SetRuleSearchUrl ->
+                _uiState.update { it.copy(rule = it.rule.copy(searchUrl = intent.value)) }
+            is CoverConfigIntent.SetRuleExpression ->
+                _uiState.update { it.copy(rule = it.rule.copy(coverRule = intent.value)) }
+            CoverConfigIntent.RestoreDefaultRule -> {
+                val rule = DefaultData.coverRule
+                _uiState.update {
+                    it.copy(
+                        rule = CoverRuleUiState(rule.enable, rule.searchUrl, rule.coverRule)
+                    )
+                }
+                _effects.tryEmit(CoverConfigEffect.ShowToast(R.string.restore_default))
+            }
+            CoverConfigIntent.SaveRule -> saveRule()
         }
         }
     }
     }
 
 
-    fun updateShowAuthor(show: Boolean, isNight: Boolean = false) {
-        if (isNight) {
-            CoverConfig.coverShowAuthorN = show
-        } else {
-            CoverConfig.coverShowAuthor = show
+    private fun loadRule() {
+        viewModelScope.launch(Dispatchers.IO) {
+            val rule = BookCover.getCoverRule()
+            _uiState.update {
+                it.copy(rule = CoverRuleUiState(rule.enable, rule.searchUrl, rule.coverRule))
+            }
         }
         }
     }
     }
 
 
-    fun updateCoverStyle() {
-        // no-op: Compose CoilBookCover reads CoverConfig preferences directly
+    private fun saveRule() {
+        val state = _uiState.value.rule
+        if (state.searchUrl.isBlank() || state.coverRule.isBlank()) {
+            _effects.tryEmit(CoverConfigEffect.ShowToast(R.string.cover_rule_fields_required))
+            return
+        }
+        viewModelScope.launch(Dispatchers.IO) {
+            val rule = BookCover.CoverRule(state.enabled, state.searchUrl, state.coverRule)
+            if (rule == DefaultData.coverRule) BookCover.delCoverRule() else BookCover.saveCoverRule(rule)
+            _uiState.update { it.copy(activeSheet = null) }
+        }
     }
     }
 }
 }
 
 
@@ -59,15 +107,9 @@ internal fun io.legado.app.domain.model.CoverAlbum.toUi() = CoverAlbumItemUi(
     id = id,
     id = id,
     name = name,
     name = name,
     lightImages = lightImages.map {
     lightImages = lightImages.map {
-        CoverAlbumImageUi(
-            id = it.id,
-            path = it.path,
-        )
+        CoverAlbumImageUi(id = it.id, path = it.path)
     }.toImmutableList(),
     }.toImmutableList(),
     darkImages = darkImages.map {
     darkImages = darkImages.map {
-        CoverAlbumImageUi(
-            id = it.id,
-            path = it.path,
-        )
+        CoverAlbumImageUi(id = it.id, path = it.path)
     }.toImmutableList(),
     }.toImmutableList(),
 )
 )

+ 11 - 57
app/src/main/java/io/legado/app/ui/config/coverConfig/CoverRuleConfigSheet.kt

@@ -11,49 +11,23 @@ import androidx.compose.material.icons.filled.Save
 import androidx.compose.material.icons.filled.SettingsBackupRestore
 import androidx.compose.material.icons.filled.SettingsBackupRestore
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.R
-import io.legado.app.help.DefaultData
-import io.legado.app.model.BookCover
 import io.legado.app.ui.widget.components.AppTextField
 import io.legado.app.ui.widget.components.AppTextField
 import io.legado.app.ui.widget.components.button.series.MediumPlainButton
 import io.legado.app.ui.widget.components.button.series.MediumPlainButton
 import io.legado.app.ui.widget.components.checkBox.CheckboxItem
 import io.legado.app.ui.widget.components.checkBox.CheckboxItem
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
-import io.legado.app.utils.toastOnUi
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import splitties.init.appCtx
 
 
 @OptIn(ExperimentalMaterial3Api::class)
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 @Composable
 fun CoverRuleConfigSheet(
 fun CoverRuleConfigSheet(
     show: Boolean,
     show: Boolean,
-    onDismissRequest: () -> Unit
+    state: CoverRuleUiState,
+    onIntent: (CoverConfigIntent) -> Unit,
+    onDismissRequest: () -> Unit,
 ) {
 ) {
-    var ruleState by remember { mutableStateOf<BookCover.CoverRule?>(null) }
-    var enable by remember { mutableStateOf(false) }
-    var searchUrl by remember { mutableStateOf("") }
-    var coverRule by remember { mutableStateOf("") }
-
-    LaunchedEffect(show) {
-        if (show) {
-            val rule = withContext(Dispatchers.IO) {
-                BookCover.getCoverRule()
-            }
-            ruleState = rule
-            enable = rule.enable
-            searchUrl = rule.searchUrl
-            coverRule = rule.coverRule
-        }
-    }
-
     AppModalBottomSheet(
     AppModalBottomSheet(
         show = show,
         show = show,
         onDismissRequest = onDismissRequest,
         onDismissRequest = onDismissRequest,
@@ -63,12 +37,7 @@ fun CoverRuleConfigSheet(
             icon = Icons.Default.SettingsBackupRestore,
             icon = Icons.Default.SettingsBackupRestore,
             contentDescription = stringResource(R.string.restore_default),
             contentDescription = stringResource(R.string.restore_default),
                 onClick = {
                 onClick = {
-                    DefaultData.coverRule.let {
-                        enable = it.enable
-                        searchUrl = it.searchUrl
-                        coverRule = it.coverRule
-                    }
-                    appCtx.toastOnUi(R.string.restore_default)
+                    onIntent(CoverConfigIntent.RestoreDefaultRule)
                 }
                 }
             )
             )
         },
         },
@@ -77,22 +46,7 @@ fun CoverRuleConfigSheet(
             icon = Icons.Default.Save,
             icon = Icons.Default.Save,
             contentDescription = stringResource(R.string.save),
             contentDescription = stringResource(R.string.save),
                 onClick = {
                 onClick = {
-                    if (searchUrl.isBlank() || coverRule.isBlank()) {
-                        appCtx.toastOnUi(R.string.cover_rule_fields_required)
-                    } else {
-                        val newConfig = ruleState?.copy(
-                            enable = enable,
-                            searchUrl = searchUrl,
-                            coverRule = coverRule
-                        ) ?: BookCover.CoverRule(enable, searchUrl, coverRule)
-
-                        if (newConfig == DefaultData.coverRule) {
-                            BookCover.delCoverRule()
-                        } else {
-                            BookCover.saveCoverRule(newConfig)
-                        }
-                        onDismissRequest()
-                    }
+                    onIntent(CoverConfigIntent.SaveRule)
                 }
                 }
             )
             )
         }
         }
@@ -106,20 +60,20 @@ fun CoverRuleConfigSheet(
         ) {
         ) {
             CheckboxItem(
             CheckboxItem(
                 title = stringResource(R.string.enable),
                 title = stringResource(R.string.enable),
-                checked = enable,
-                onCheckedChange = { enable = it }
+                checked = state.enabled,
+                onCheckedChange = { onIntent(CoverConfigIntent.SetRuleEnabled(it)) }
             )
             )
 
 
             AppTextField(
             AppTextField(
-                value = searchUrl,
-                onValueChange = { searchUrl = it },
+                value = state.searchUrl,
+                onValueChange = { onIntent(CoverConfigIntent.SetRuleSearchUrl(it)) },
                 label = stringResource(R.string.search_via_url),
                 label = stringResource(R.string.search_via_url),
                 modifier = Modifier.fillMaxWidth()
                 modifier = Modifier.fillMaxWidth()
             )
             )
 
 
             AppTextField(
             AppTextField(
-                value = coverRule,
-                onValueChange = { coverRule = it },
+                value = state.coverRule,
+                onValueChange = { onIntent(CoverConfigIntent.SetRuleExpression(it)) },
                 label = stringResource(R.string.cover_rule_edit),
                 label = stringResource(R.string.cover_rule_edit),
                 modifier = Modifier.fillMaxWidth(),
                 modifier = Modifier.fillMaxWidth(),
                 minLines = 3
                 minLines = 3

+ 34 - 0
app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfigContract.kt

@@ -0,0 +1,34 @@
+package io.legado.app.ui.config.downloadCacheConfig
+
+import androidx.compose.runtime.Stable
+import io.legado.app.domain.model.settings.DownloadCacheSettings
+
+@Stable
+data class DownloadCacheConfigUiState(
+    val settings: DownloadCacheSettings = DownloadCacheSettings(),
+    val coverCacheSizeMb: Double = 0.0,
+    val mangaCacheSizeMb: Double = 0.0,
+    val dialog: DownloadCacheConfigDialog? = null,
+)
+
+enum class DownloadCacheConfigDialog {
+    ClearCoverCache,
+    ClearMangaCache,
+    ClearBookCache,
+    ShrinkDatabase,
+}
+
+sealed interface DownloadCacheConfigIntent {
+    data class SetThreadCount(val value: Int) : DownloadCacheConfigIntent
+    data class SetCacheBookThreadCount(val value: Int) : DownloadCacheConfigIntent
+    data class SetPreDownloadNum(val value: Int) : DownloadCacheConfigIntent
+    data class SetBitmapCacheSize(val value: Int) : DownloadCacheConfigIntent
+    data class SetImageRetainNum(val value: Int) : DownloadCacheConfigIntent
+    data class SetUserAgent(val value: String) : DownloadCacheConfigIntent
+    data class SetCronetEnabled(val value: Boolean) : DownloadCacheConfigIntent
+    data class ShowDialog(val dialog: DownloadCacheConfigDialog) : DownloadCacheConfigIntent
+    data object DismissDialog : DownloadCacheConfigIntent
+    data object ConfirmDialog : DownloadCacheConfigIntent
+}
+
+sealed interface DownloadCacheConfigEffect

+ 89 - 56
app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfigScreen.kt

@@ -4,13 +4,8 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.R
@@ -27,21 +22,30 @@ import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+
+@Composable
+fun DownloadCacheConfigRouteScreen(
+    onBackClick: () -> Unit,
+    viewModel: DownloadCacheConfigViewModel = koinViewModel(),
+) {
+    DownloadCacheConfigScreen(
+        state = viewModel.uiState.collectAsStateWithLifecycle().value,
+        onIntent = viewModel::onIntent,
+        onBackClick = onBackClick,
+    )
+}
 
 
 @OptIn(ExperimentalMaterial3Api::class)
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 @Composable
 fun DownloadCacheConfigScreen(
 fun DownloadCacheConfigScreen(
+    state: DownloadCacheConfigUiState,
+    onIntent: (DownloadCacheConfigIntent) -> Unit,
     onBackClick: () -> Unit,
     onBackClick: () -> Unit,
-    viewModel: DownloadCacheConfigViewModel = koinViewModel()
 ) {
 ) {
-    val context = LocalContext.current
+    val settings = state.settings
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
 
 
-    var showClearBookCacheDialog by remember { mutableStateOf(false) }
-    var showShrinkDbDialog by remember { mutableStateOf(false) }
-    var showClearCoverCacheDialog by remember { mutableStateOf(false) }
-    var showClearMangaCacheDialog by remember { mutableStateOf(false) }
-
     AppScaffold(
     AppScaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         topBar = {
         topBar = {
@@ -67,17 +71,29 @@ fun DownloadCacheConfigScreen(
                         title = stringResource(R.string.cover_cache),
                         title = stringResource(R.string.cover_cache),
                         description = stringResource(
                         description = stringResource(
                             R.string.cache_size_mb,
                             R.string.cache_size_mb,
-                            viewModel.coverCacheSize
+                            state.coverCacheSizeMb
                         ),
                         ),
-                        onClick = { showClearCoverCacheDialog = true }
+                        onClick = {
+                            onIntent(
+                                DownloadCacheConfigIntent.ShowDialog(
+                                    DownloadCacheConfigDialog.ClearCoverCache
+                                )
+                            )
+                        }
                     )
                     )
                     ClickableSettingItem(
                     ClickableSettingItem(
                         title = stringResource(R.string.manga_cache),
                         title = stringResource(R.string.manga_cache),
                         description = stringResource(
                         description = stringResource(
                             R.string.cache_size_mb,
                             R.string.cache_size_mb,
-                            viewModel.mangaCacheSize
+                            state.mangaCacheSizeMb
                         ),
                         ),
-                        onClick = { showClearMangaCacheDialog = true }
+                        onClick = {
+                            onIntent(
+                                DownloadCacheConfigIntent.ShowDialog(
+                                    DownloadCacheConfigDialog.ClearMangaCache
+                                )
+                            )
+                        }
                     )
                     )
                 }
                 }
 
 
@@ -85,23 +101,26 @@ fun DownloadCacheConfigScreen(
                     SliderSettingItem(
                     SliderSettingItem(
                         title = stringResource(R.string.threads_num_title),
                         title = stringResource(R.string.threads_num_title),
                         description = stringResource(R.string.threads_num_summary),
                         description = stringResource(R.string.threads_num_summary),
-                        value = DownloadCacheConfig.threadCount.toFloat(),
+                        value = settings.threadCount.toFloat(),
                         defaultValue = 8f,
                         defaultValue = 8f,
                         valueRange = 1f..256f,
                         valueRange = 1f..256f,
-                        onValueChange = { DownloadCacheConfig.threadCount = it.toInt() }
+                        onValueChange = {
+                            onIntent(DownloadCacheConfigIntent.SetThreadCount(it.toInt()))
+                        }
                     )
                     )
 
 
                     SliderSettingItem(
                     SliderSettingItem(
                         title = stringResource(R.string.cache_book_threads_num_title),
                         title = stringResource(R.string.cache_book_threads_num_title),
                         description = stringResource(R.string.cache_book_threads_num_summary),
                         description = stringResource(R.string.cache_book_threads_num_summary),
-                        value = DownloadCacheConfig.cacheBookThreadCount
+                        value = settings.cacheBookThreadCount
                             .coerceIn(1, CacheBook.maxDownloadConcurrency)
                             .coerceIn(1, CacheBook.maxDownloadConcurrency)
                             .toFloat(),
                             .toFloat(),
                         defaultValue = CacheBook.maxDownloadConcurrency.toFloat(),
                         defaultValue = CacheBook.maxDownloadConcurrency.toFloat(),
                         valueRange = 1f..CacheBook.maxDownloadConcurrency.toFloat(),
                         valueRange = 1f..CacheBook.maxDownloadConcurrency.toFloat(),
                         onValueChange = {
                         onValueChange = {
-                            DownloadCacheConfig.cacheBookThreadCount =
-                                it.toInt().coerceIn(1, CacheBook.maxDownloadConcurrency)
+                            onIntent(
+                                DownloadCacheConfigIntent.SetCacheBookThreadCount(it.toInt())
+                            )
                         }
                         }
                     )
                     )
 
 
@@ -109,12 +128,14 @@ fun DownloadCacheConfigScreen(
                         title = stringResource(R.string.pre_download),
                         title = stringResource(R.string.pre_download),
                         description = stringResource(
                         description = stringResource(
                             R.string.pre_download_s,
                             R.string.pre_download_s,
-                            DownloadCacheConfig.preDownloadNum
+                            settings.preDownloadNum
                         ),
                         ),
-                        value = DownloadCacheConfig.preDownloadNum.toFloat(),
+                        value = settings.preDownloadNum.toFloat(),
                         defaultValue = 10f,
                         defaultValue = 10f,
                         valueRange = 0f..100f,
                         valueRange = 0f..100f,
-                        onValueChange = { DownloadCacheConfig.preDownloadNum = it.toInt() }
+                        onValueChange = {
+                            onIntent(DownloadCacheConfigIntent.SetPreDownloadNum(it.toInt()))
+                        }
                     )
                     )
                 }
                 }
 
 
@@ -123,13 +144,13 @@ fun DownloadCacheConfigScreen(
                         title = stringResource(R.string.bitmap_cache_size),
                         title = stringResource(R.string.bitmap_cache_size),
                         description = stringResource(
                         description = stringResource(
                             R.string.bitmap_cache_size_summary,
                             R.string.bitmap_cache_size_summary,
-                            DownloadCacheConfig.bitmapCacheSize
+                            settings.bitmapCacheSize
                         ),
                         ),
-                        value = DownloadCacheConfig.bitmapCacheSize.toFloat(),
+                        value = settings.bitmapCacheSize.toFloat(),
                         defaultValue = 32f,
                         defaultValue = 32f,
                         valueRange = 1f..2047f,
                         valueRange = 1f..2047f,
                         onValueChange = {
                         onValueChange = {
-                            viewModel.updateBitmapCacheSize(it.toInt())
+                            onIntent(DownloadCacheConfigIntent.SetBitmapCacheSize(it.toInt()))
                         }
                         }
                     )
                     )
 
 
@@ -137,27 +158,31 @@ fun DownloadCacheConfigScreen(
                         title = stringResource(R.string.image_retain_number),
                         title = stringResource(R.string.image_retain_number),
                         description = stringResource(
                         description = stringResource(
                             R.string.image_retain_number_summary,
                             R.string.image_retain_number_summary,
-                            DownloadCacheConfig.imageRetainNum
+                            settings.imageRetainNum
                         ),
                         ),
-                        value = DownloadCacheConfig.imageRetainNum.toFloat(),
+                        value = settings.imageRetainNum.toFloat(),
                         defaultValue = 10f,
                         defaultValue = 10f,
                         valueRange = 0f..100f,
                         valueRange = 0f..100f,
-                        onValueChange = { DownloadCacheConfig.imageRetainNum = it.toInt() }
+                        onValueChange = {
+                            onIntent(DownloadCacheConfigIntent.SetImageRetainNum(it.toInt()))
+                        }
                     )
                     )
                 }
                 }
 
 
                 SplicedColumnGroup(title = stringResource(R.string.network)) {
                 SplicedColumnGroup(title = stringResource(R.string.network)) {
                     InputSettingItem(
                     InputSettingItem(
                         title = stringResource(R.string.user_agent),
                         title = stringResource(R.string.user_agent),
-                        value = DownloadCacheConfig.userAgent,
-                        onConfirm = { viewModel.saveUserAgent(it) }
+                        value = settings.userAgent,
+                        onConfirm = { onIntent(DownloadCacheConfigIntent.SetUserAgent(it)) }
                     )
                     )
 
 
                     SwitchSettingItem(
                     SwitchSettingItem(
                         title = "Cronet",
                         title = "Cronet",
                         description = stringResource(R.string.pref_cronet_summary),
                         description = stringResource(R.string.pref_cronet_summary),
-                        checked = DownloadCacheConfig.cronetEnable,
-                        onCheckedChange = { DownloadCacheConfig.cronetEnable = it }
+                        checked = settings.cronetEnabled,
+                        onCheckedChange = {
+                            onIntent(DownloadCacheConfigIntent.SetCronetEnabled(it))
+                        }
                     )
                     )
                 }
                 }
 
 
@@ -165,64 +190,72 @@ fun DownloadCacheConfigScreen(
                     ClickableSettingItem(
                     ClickableSettingItem(
                         title = stringResource(R.string.clear_cache),
                         title = stringResource(R.string.clear_cache),
                         description = stringResource(R.string.clear_cache_summary),
                         description = stringResource(R.string.clear_cache_summary),
-                        onClick = { showClearBookCacheDialog = true }
+                        onClick = {
+                            onIntent(
+                                DownloadCacheConfigIntent.ShowDialog(
+                                    DownloadCacheConfigDialog.ClearBookCache
+                                )
+                            )
+                        }
                     )
                     )
 
 
                     ClickableSettingItem(
                     ClickableSettingItem(
                         title = stringResource(R.string.shrink_database),
                         title = stringResource(R.string.shrink_database),
                         description = stringResource(R.string.shrink_database_summary),
                         description = stringResource(R.string.shrink_database_summary),
-                        onClick = { showShrinkDbDialog = true }
+                        onClick = {
+                            onIntent(
+                                DownloadCacheConfigIntent.ShowDialog(
+                                    DownloadCacheConfigDialog.ShrinkDatabase
+                                )
+                            )
+                        }
                     )
                     )
                 }
                 }
             }
             }
         }
         }
 
 
         AppAlertDialog(
         AppAlertDialog(
-            show = showClearBookCacheDialog,
-            onDismissRequest = { showClearBookCacheDialog = false },
+            show = state.dialog == DownloadCacheConfigDialog.ClearBookCache,
+            onDismissRequest = { onIntent(DownloadCacheConfigIntent.DismissDialog) },
             title = stringResource(R.string.clear_cache),
             title = stringResource(R.string.clear_cache),
             text = stringResource(R.string.sure_del),
             text = stringResource(R.string.sure_del),
             onConfirm = {
             onConfirm = {
-                viewModel.clearBookCache(context)
-                showClearBookCacheDialog = false
+                onIntent(DownloadCacheConfigIntent.ConfirmDialog)
             },
             },
-            onDismiss = { showClearBookCacheDialog = false }
+            onDismiss = { onIntent(DownloadCacheConfigIntent.DismissDialog) }
         )
         )
 
 
         AppAlertDialog(
         AppAlertDialog(
-            show = showClearCoverCacheDialog,
-            onDismissRequest = { showClearCoverCacheDialog = false },
+            show = state.dialog == DownloadCacheConfigDialog.ClearCoverCache,
+            onDismissRequest = { onIntent(DownloadCacheConfigIntent.DismissDialog) },
             title = stringResource(R.string.cover_cache),
             title = stringResource(R.string.cover_cache),
             text = stringResource(R.string.sure_del),
             text = stringResource(R.string.sure_del),
             onConfirm = {
             onConfirm = {
-                viewModel.clearCoverCache()
-                showClearCoverCacheDialog = false
+                onIntent(DownloadCacheConfigIntent.ConfirmDialog)
             },
             },
-            onDismiss = { showClearCoverCacheDialog = false }
+            onDismiss = { onIntent(DownloadCacheConfigIntent.DismissDialog) }
         )
         )
 
 
         AppAlertDialog(
         AppAlertDialog(
-            show = showClearMangaCacheDialog,
-            onDismissRequest = { showClearMangaCacheDialog = false },
+            show = state.dialog == DownloadCacheConfigDialog.ClearMangaCache,
+            onDismissRequest = { onIntent(DownloadCacheConfigIntent.DismissDialog) },
             title = stringResource(R.string.manga_cache),
             title = stringResource(R.string.manga_cache),
             text = stringResource(R.string.sure_del),
             text = stringResource(R.string.sure_del),
             onConfirm = {
             onConfirm = {
-                viewModel.clearMangaCache()
-                showClearMangaCacheDialog = false
+                onIntent(DownloadCacheConfigIntent.ConfirmDialog)
             },
             },
-            onDismiss = { showClearMangaCacheDialog = false }
+            onDismiss = { onIntent(DownloadCacheConfigIntent.DismissDialog) }
         )
         )
 
 
         AppAlertDialog(
         AppAlertDialog(
-            show = showShrinkDbDialog,
-            onDismissRequest = { showShrinkDbDialog = false },
+            show = state.dialog == DownloadCacheConfigDialog.ShrinkDatabase,
+            onDismissRequest = { onIntent(DownloadCacheConfigIntent.DismissDialog) },
             title = stringResource(R.string.shrink_database),
             title = stringResource(R.string.shrink_database),
             text = stringResource(R.string.sure),
             text = stringResource(R.string.sure),
             onConfirm = {
             onConfirm = {
-                viewModel.shrinkDatabase()
-                showShrinkDbDialog = false
+                onIntent(DownloadCacheConfigIntent.ConfirmDialog)
             },
             },
-            onDismiss = { showShrinkDbDialog = false }
+            onDismiss = { onIntent(DownloadCacheConfigIntent.DismissDialog) }
         )
         )
     }
     }
 }
 }

+ 79 - 39
app/src/main/java/io/legado/app/ui/config/downloadCacheConfig/DownloadCacheConfigViewModel.kt

@@ -1,75 +1,115 @@
 package io.legado.app.ui.config.downloadCacheConfig
 package io.legado.app.ui.config.downloadCacheConfig
 
 
-import android.content.Context
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableDoubleStateOf
-import androidx.compose.runtime.setValue
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import androidx.lifecycle.viewModelScope
+import io.legado.app.domain.gateway.DownloadCacheSettingsGateway
+import io.legado.app.domain.gateway.DownloadCacheSettingsUpdate
 import io.legado.app.domain.usecase.ClearBookCacheUseCase
 import io.legado.app.domain.usecase.ClearBookCacheUseCase
 import io.legado.app.domain.usecase.ShrinkDatabaseUseCase
 import io.legado.app.domain.usecase.ShrinkDatabaseUseCase
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.http.HttpCacheType
 import io.legado.app.help.http.HttpCacheType
 import io.legado.app.help.http.clearHttpCache
 import io.legado.app.help.http.clearHttpCache
 import io.legado.app.help.http.getHttpCacheSize
 import io.legado.app.help.http.getHttpCacheSize
+import io.legado.app.model.CacheBook
 import io.legado.app.model.ImageProvider
 import io.legado.app.model.ImageProvider
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.FileUtils
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
+import splitties.init.appCtx
 
 
 class DownloadCacheConfigViewModel(
 class DownloadCacheConfigViewModel(
     private val clearBookCacheUseCase: ClearBookCacheUseCase,
     private val clearBookCacheUseCase: ClearBookCacheUseCase,
-    private val shrinkDatabaseUseCase: ShrinkDatabaseUseCase
+    private val shrinkDatabaseUseCase: ShrinkDatabaseUseCase,
+    private val settingsGateway: DownloadCacheSettingsGateway,
 ) : ViewModel() {
 ) : ViewModel() {
 
 
-    var coverCacheSize by mutableDoubleStateOf(0.0)
-    var mangaCacheSize by mutableDoubleStateOf(0.0)
+    private val _uiState = MutableStateFlow(DownloadCacheConfigUiState())
+    val uiState = _uiState.asStateFlow()
 
 
     init {
     init {
-        loadCacheSizes()
-    }
-
-    fun loadCacheSizes() {
-        viewModelScope.launch(Dispatchers.IO) {
-            coverCacheSize = getHttpCacheSize(HttpCacheType.COVER) / (1024.0 * 1024.0)
-            mangaCacheSize = getHttpCacheSize(HttpCacheType.MANGA) / (1024.0 * 1024.0)
+        viewModelScope.launch {
+            settingsGateway.settings.collect { settings ->
+                _uiState.update { it.copy(settings = settings) }
+            }
         }
         }
+        loadCacheSizes()
     }
     }
 
 
-    fun clearCoverCache() {
-        viewModelScope.launch(Dispatchers.IO) {
-            clearHttpCache(HttpCacheType.COVER)
-            coverCacheSize = 0.0
+    fun onIntent(intent: DownloadCacheConfigIntent) {
+        when (intent) {
+            is DownloadCacheConfigIntent.SetThreadCount -> update(
+                DownloadCacheSettingsUpdate.ThreadCount(intent.value)
+            )
+            is DownloadCacheConfigIntent.SetCacheBookThreadCount -> update(
+                DownloadCacheSettingsUpdate.CacheBookThreadCount(
+                    intent.value.coerceIn(1, CacheBook.maxDownloadConcurrency)
+                )
+            )
+            is DownloadCacheConfigIntent.SetPreDownloadNum -> update(
+                DownloadCacheSettingsUpdate.PreDownloadNum(intent.value)
+            )
+            is DownloadCacheConfigIntent.SetBitmapCacheSize -> {
+                viewModelScope.launch {
+                    settingsGateway.update(
+                        DownloadCacheSettingsUpdate.BitmapCacheSize(intent.value)
+                    )
+                    ImageProvider.bitmapLruCache.resize(ImageProvider.cacheSize)
+                }
+            }
+            is DownloadCacheConfigIntent.SetImageRetainNum -> update(
+                DownloadCacheSettingsUpdate.ImageRetainNum(intent.value)
+            )
+            is DownloadCacheConfigIntent.SetUserAgent -> update(
+                DownloadCacheSettingsUpdate.UserAgent(intent.value)
+            )
+            is DownloadCacheConfigIntent.SetCronetEnabled -> update(
+                DownloadCacheSettingsUpdate.CronetEnabled(intent.value)
+            )
+            is DownloadCacheConfigIntent.ShowDialog ->
+                _uiState.update { it.copy(dialog = intent.dialog) }
+            DownloadCacheConfigIntent.DismissDialog ->
+                _uiState.update { it.copy(dialog = null) }
+            DownloadCacheConfigIntent.ConfirmDialog -> confirmDialog()
         }
         }
     }
     }
 
 
-    fun clearMangaCache() {
-        viewModelScope.launch(Dispatchers.IO) {
-            clearHttpCache(HttpCacheType.MANGA)
-            mangaCacheSize = 0.0
-        }
+    private fun update(update: DownloadCacheSettingsUpdate) {
+        viewModelScope.launch { settingsGateway.update(update) }
     }
     }
 
 
-    fun clearBookCache(context: Context) {
+    private fun loadCacheSizes() {
         viewModelScope.launch(Dispatchers.IO) {
         viewModelScope.launch(Dispatchers.IO) {
-            clearBookCacheUseCase.executeAll()
-            FileUtils.delete(context.cacheDir.absolutePath)
-            context.externalCacheDir?.deleteRecursively()
+            _uiState.update {
+                it.copy(
+                    coverCacheSizeMb = getHttpCacheSize(HttpCacheType.COVER) / (1024.0 * 1024.0),
+                    mangaCacheSizeMb = getHttpCacheSize(HttpCacheType.MANGA) / (1024.0 * 1024.0),
+                )
+            }
         }
         }
     }
     }
 
 
-    fun shrinkDatabase() {
+    private fun confirmDialog() {
+        val dialog = _uiState.value.dialog ?: return
+        _uiState.update { it.copy(dialog = null) }
         viewModelScope.launch(Dispatchers.IO) {
         viewModelScope.launch(Dispatchers.IO) {
-            shrinkDatabaseUseCase.execute()
+            when (dialog) {
+                DownloadCacheConfigDialog.ClearCoverCache -> {
+                    clearHttpCache(HttpCacheType.COVER)
+                    _uiState.update { it.copy(coverCacheSizeMb = 0.0) }
+                }
+                DownloadCacheConfigDialog.ClearMangaCache -> {
+                    clearHttpCache(HttpCacheType.MANGA)
+                    _uiState.update { it.copy(mangaCacheSizeMb = 0.0) }
+                }
+                DownloadCacheConfigDialog.ClearBookCache -> {
+                    clearBookCacheUseCase.executeAll()
+                    FileUtils.delete(appCtx.cacheDir.absolutePath)
+                    appCtx.externalCacheDir?.deleteRecursively()
+                }
+                DownloadCacheConfigDialog.ShrinkDatabase -> shrinkDatabaseUseCase.execute()
+            }
         }
         }
     }
     }
-
-    fun updateBitmapCacheSize(size: Int) {
-        AppConfig.bitmapCacheSize = size
-        ImageProvider.bitmapLruCache.resize(ImageProvider.cacheSize)
-    }
-
-    fun saveUserAgent(input: String) {
-        DownloadCacheConfig.userAgent = input
-    }
 }
 }

+ 17 - 0
app/src/main/java/io/legado/app/ui/config/labConfig/LabConfigContract.kt

@@ -0,0 +1,17 @@
+package io.legado.app.ui.config.labConfig
+
+import androidx.compose.runtime.Stable
+import io.legado.app.domain.model.settings.LabSettings
+
+@Stable
+data class LabConfigUiState(
+    val settings: LabSettings = LabSettings(),
+)
+
+sealed interface LabConfigIntent {
+    data class SetEnabled(val value: Boolean) : LabConfigIntent
+    data class SetEInkDisplay(val value: Boolean) : LabConfigIntent
+    data class SetEyeProtection(val value: Boolean) : LabConfigIntent
+}
+
+sealed interface LabConfigEffect

+ 43 - 41
app/src/main/java/io/legado/app/ui/config/labConfig/LabConfigScreen.kt

@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.theme.adaptiveContentPadding
@@ -22,83 +23,74 @@ import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
+import org.koin.androidx.compose.koinViewModel
+
+@Composable
+fun LabConfigRouteScreen(
+    onBackClick: () -> Unit,
+    viewModel: LabConfigViewModel = koinViewModel(),
+) {
+    LabConfigScreen(
+        state = viewModel.uiState.collectAsStateWithLifecycle().value,
+        onIntent = viewModel::onIntent,
+        onBackClick = onBackClick,
+    )
+}
 
 
 @OptIn(ExperimentalMaterial3Api::class)
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 @Composable
 fun LabConfigScreen(
 fun LabConfigScreen(
-    onBackClick: () -> Unit
+    state: LabConfigUiState,
+    onIntent: (LabConfigIntent) -> Unit,
+    onBackClick: () -> Unit,
 ) {
 ) {
+    val settings = state.settings
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
-
     AppScaffold(
     AppScaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         topBar = {
         topBar = {
             GlassMediumFlexibleTopAppBar(
             GlassMediumFlexibleTopAppBar(
                 title = stringResource(R.string.lab_setting),
                 title = stringResource(R.string.lab_setting),
                 scrollBehavior = scrollBehavior,
                 scrollBehavior = scrollBehavior,
-                navigationIcon = {
-                    TopBarNavigationButton(onClick = onBackClick)
-                }
+                navigationIcon = { TopBarNavigationButton(onClick = onBackClick) },
             )
             )
-        }
+        },
     ) { paddingValues ->
     ) { paddingValues ->
         LazyColumn(
         LazyColumn(
             modifier = Modifier.fillMaxSize(),
             modifier = Modifier.fillMaxSize(),
             contentPadding = adaptiveContentPadding(
             contentPadding = adaptiveContentPadding(
                 top = paddingValues.calculateTopPadding(),
                 top = paddingValues.calculateTopPadding(),
-                bottom = 120.dp
-            )
+                bottom = 120.dp,
+            ),
         ) {
         ) {
             item {
             item {
                 SplicedColumnGroup {
                 SplicedColumnGroup {
                     SwitchSettingItem(
                     SwitchSettingItem(
                         title = stringResource(R.string.lab_enabled_title),
                         title = stringResource(R.string.lab_enabled_title),
                         description = stringResource(R.string.lab_enabled_summary),
                         description = stringResource(R.string.lab_enabled_summary),
-                        checked = LabConfig.labEnabled,
-                        onCheckedChange = { LabConfig.labEnabled = it }
+                        checked = settings.enabled,
+                        onCheckedChange = { onIntent(LabConfigIntent.SetEnabled(it)) },
                     )
                     )
                 }
                 }
-
-                AnimatedVisibility(visible = LabConfig.labEnabled) {
+                AnimatedVisibility(visible = settings.enabled) {
                     SplicedColumnGroup(title = stringResource(R.string.lab_display)) {
                     SplicedColumnGroup(title = stringResource(R.string.lab_display)) {
                         SwitchSettingItem(
                         SwitchSettingItem(
                             title = stringResource(R.string.lab_eink_display_title),
                             title = stringResource(R.string.lab_eink_display_title),
                             description = stringResource(R.string.lab_eink_display_summary),
                             description = stringResource(R.string.lab_eink_display_summary),
-                            checked = LabConfig.eInkDisplay,
-                            onCheckedChange = {
-                                LabConfig.eInkDisplay = it
-                            }
+                            checked = settings.eInkDisplay,
+                            onCheckedChange = { onIntent(LabConfigIntent.SetEInkDisplay(it)) },
                         )
                         )
-
-                        if (LabConfig.eInkDisplay) {
-                            Text(
-                                text = stringResource(R.string.lab_eink_display_hint),
-                                style = LegadoTheme.typography.labelSmall,
-                                color = MaterialTheme.colorScheme.primary,
-                                modifier = Modifier
-                                    .fillMaxWidth()
-                                    .padding(horizontal = 16.dp, vertical = 4.dp)
-                            )
+                        if (settings.eInkDisplay) {
+                            HintText(R.string.lab_eink_display_hint)
                         }
                         }
-
                         SwitchSettingItem(
                         SwitchSettingItem(
                             title = stringResource(R.string.lab_eye_protection_title),
                             title = stringResource(R.string.lab_eye_protection_title),
                             description = stringResource(R.string.lab_eye_protection_summary),
                             description = stringResource(R.string.lab_eye_protection_summary),
-                            checked = LabConfig.eyeProtection,
-                            onCheckedChange = {
-                                LabConfig.eyeProtection = it
-                            }
+                            checked = settings.eyeProtection,
+                            onCheckedChange = { onIntent(LabConfigIntent.SetEyeProtection(it)) },
                         )
                         )
-
-                        if (LabConfig.eyeProtection) {
-                            Text(
-                                text = stringResource(R.string.lab_eye_protection_hint),
-                                style = LegadoTheme.typography.labelSmall,
-                                color = MaterialTheme.colorScheme.primary,
-                                modifier = Modifier
-                                    .fillMaxWidth()
-                                    .padding(horizontal = 16.dp, vertical = 4.dp)
-                            )
+                        if (settings.eyeProtection) {
+                            HintText(R.string.lab_eye_protection_hint)
                         }
                         }
                     }
                     }
                 }
                 }
@@ -106,3 +98,13 @@ fun LabConfigScreen(
         }
         }
     }
     }
 }
 }
+
+@Composable
+private fun HintText(textRes: Int) {
+    Text(
+        text = stringResource(textRes),
+        style = LegadoTheme.typography.labelSmall,
+        color = MaterialTheme.colorScheme.primary,
+        modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 4.dp),
+    )
+}

+ 39 - 0
app/src/main/java/io/legado/app/ui/config/labConfig/LabConfigViewModel.kt

@@ -0,0 +1,39 @@
+package io.legado.app.ui.config.labConfig
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import io.legado.app.domain.gateway.LabSettingsGateway
+import io.legado.app.domain.gateway.LabSettingsUpdate
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+class LabConfigViewModel(
+    private val settingsGateway: LabSettingsGateway,
+) : ViewModel() {
+    private val _uiState = MutableStateFlow(LabConfigUiState())
+    val uiState = _uiState.asStateFlow()
+
+    private val _effects = MutableSharedFlow<LabConfigEffect>(extraBufferCapacity = 16)
+    val effects = _effects.asSharedFlow()
+
+    init {
+        viewModelScope.launch {
+            settingsGateway.settings.collect { settings ->
+                _uiState.update { it.copy(settings = settings) }
+            }
+        }
+    }
+
+    fun onIntent(intent: LabConfigIntent) {
+        val update = when (intent) {
+            is LabConfigIntent.SetEnabled -> LabSettingsUpdate.Enabled(intent.value)
+            is LabConfigIntent.SetEInkDisplay -> LabSettingsUpdate.EInkDisplay(intent.value)
+            is LabConfigIntent.SetEyeProtection -> LabSettingsUpdate.EyeProtection(intent.value)
+        }
+        viewModelScope.launch { settingsGateway.update(update) }
+    }
+}

+ 5 - 27
app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt

@@ -1,7 +1,5 @@
 package io.legado.app.ui.config.themeConfig
 package io.legado.app.ui.config.themeConfig
 
 
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.result.contract.ActivityResultContracts
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.aspectRatio
 import androidx.compose.foundation.layout.aspectRatio
@@ -17,7 +15,6 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
@@ -32,42 +29,23 @@ import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
 import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
 import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
-import kotlinx.coroutines.launch
-import org.koin.androidx.compose.koinViewModel
 
 
 @OptIn(ExperimentalMaterial3Api::class)
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 @Composable
 fun BackgroundImageManageSheet(
 fun BackgroundImageManageSheet(
     isDarkTheme: Boolean?,
     isDarkTheme: Boolean?,
+    currentPath: String?,
     onDismissRequest: () -> Unit,
     onDismissRequest: () -> Unit,
-    viewModel: ThemeConfigViewModel = koinViewModel()
+    onSelectImage: (Boolean) -> Unit,
+    onRemoveImage: (Boolean) -> Unit,
 ) {
 ) {
-    val scope = rememberCoroutineScope()
     var showFilePicker by remember { mutableStateOf(false) }
     var showFilePicker by remember { mutableStateOf(false) }
 
 
-    val selectImage =
-        rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
-            uri?.let {
-                scope.launch {
-                    viewModel.setBackgroundFromUri(
-                        uri = it,
-                        isDarkTheme = isDarkTheme == true
-                    )
-                }
-            }
-        }
-
     AppModalBottomSheet(
     AppModalBottomSheet(
         data = isDarkTheme,
         data = isDarkTheme,
         onDismissRequest = onDismissRequest,
         onDismissRequest = onDismissRequest,
         title = stringResource(R.string.background_image),
         title = stringResource(R.string.background_image),
     ) { isDark ->
     ) { isDark ->
-        val currentPath = if (isDark) {
-            ThemeConfig.bgImageDark
-        } else {
-            ThemeConfig.bgImageLight
-        }
-
         Column(
         Column(
             modifier = Modifier
             modifier = Modifier
                 .fillMaxWidth()
                 .fillMaxWidth()
@@ -115,7 +93,7 @@ fun BackgroundImageManageSheet(
                         )
                         )
                     }
                     }
                     SmallTonalButton(
                     SmallTonalButton(
-                        onClick = { viewModel.removeBackground(isDark) },
+                        onClick = { onRemoveImage(isDark) },
                         modifier = Modifier
                         modifier = Modifier
                             .align(Alignment.TopEnd)
                             .align(Alignment.TopEnd)
                             .padding(8.dp)
                             .padding(8.dp)
@@ -132,7 +110,7 @@ fun BackgroundImageManageSheet(
         show = showFilePicker,
         show = showFilePicker,
         onDismissRequest = { showFilePicker = false },
         onDismissRequest = { showFilePicker = false },
         onSelectSysFile = {
         onSelectSysFile = {
-            selectImage.launch("image/*")
+            onSelectImage(isDarkTheme == true)
             showFilePicker = false
             showFilePicker = false
         },
         },
         allowExtensions = arrayOf("jpg", "jpeg", "png", "webp")
         allowExtensions = arrayOf("jpg", "jpeg", "png", "webp")

+ 15 - 28
app/src/main/java/io/legado/app/ui/config/themeConfig/MainNavigationSettingsSheet.kt

@@ -19,6 +19,7 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.R
+import io.legado.app.domain.model.settings.AppShellSettings
 import io.legado.app.ui.main.MainDestination
 import io.legado.app.ui.main.MainDestination
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
 import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
@@ -30,10 +31,14 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
 @Composable
 @Composable
 fun MainNavigationSettingsSheet(
 fun MainNavigationSettingsSheet(
     show: Boolean,
     show: Boolean,
+    settings: AppShellSettings,
     onDismissRequest: () -> Unit,
     onDismissRequest: () -> Unit,
+    onSetVisible: (String, Boolean) -> Unit,
+    onSetOrder: (String) -> Unit,
+    onSetDefault: (String) -> Unit,
 ) {
 ) {
     var navigationItems by remember(show) {
     var navigationItems by remember(show) {
-        mutableStateOf(MainDestination.ordered(ThemeConfig.mainNavigationOrder))
+        mutableStateOf(MainDestination.ordered(settings.mainNavigationOrder))
     }
     }
     val navigationListState = rememberLazyListState()
     val navigationListState = rememberLazyListState()
     val reorderableState =
     val reorderableState =
@@ -45,48 +50,31 @@ fun MainNavigationSettingsSheet(
 
 
     LaunchedEffect(reorderableState.isAnyItemDragging) {
     LaunchedEffect(reorderableState.isAnyItemDragging) {
         if (!reorderableState.isAnyItemDragging) {
         if (!reorderableState.isAnyItemDragging) {
-            ThemeConfig.mainNavigationOrder =
-                navigationItems.joinToString(",") { it.route }
-        }
-    }
-
-    fun updateVisibility(
-        route: String,
-        visible: Boolean,
-        update: (Boolean) -> Unit,
-    ) {
-        update(visible)
-        if (!visible && ThemeConfig.defaultHomePage == route) {
-            ThemeConfig.defaultHomePage = MainDestination.Bookshelf.route
+            onSetOrder(navigationItems.joinToString(",") { it.route })
         }
         }
     }
     }
 
 
     fun isRouteVisible(route: String): Boolean = when (route) {
     fun isRouteVisible(route: String): Boolean = when (route) {
-        MainDestination.Home.route -> ThemeConfig.showHome
-        MainDestination.Explore.route -> ThemeConfig.showDiscovery
-        MainDestination.Rss.route -> ThemeConfig.showRss
+        MainDestination.Home.route -> settings.showHome
+        MainDestination.Explore.route -> settings.showDiscovery
+        MainDestination.Rss.route -> settings.showRss
         else -> true
         else -> true
     }
     }
 
 
     fun getVisibilityForRoute(route: String): Boolean = isRouteVisible(route)
     fun getVisibilityForRoute(route: String): Boolean = isRouteVisible(route)
 
 
     fun setVisibilityForRoute(route: String, visible: Boolean) {
     fun setVisibilityForRoute(route: String, visible: Boolean) {
-        when (route) {
-            MainDestination.Home.route -> updateVisibility(route, visible) { ThemeConfig.showHome = it }
-            MainDestination.Explore.route -> updateVisibility(route, visible) { ThemeConfig.showDiscovery = it }
-            MainDestination.Rss.route -> updateVisibility(route, visible) { ThemeConfig.showRss = it }
-        }
+        onSetVisible(route, visible)
         if (!visible) {
         if (!visible) {
             val item = navigationItems.find { it.route == route } ?: return
             val item = navigationItems.find { it.route == route } ?: return
             navigationItems = navigationItems.filter { it.route != route } + item
             navigationItems = navigationItems.filter { it.route != route } + item
-            ThemeConfig.mainNavigationOrder =
-                navigationItems.joinToString(",") { it.route }
+            onSetOrder(navigationItems.joinToString(",") { it.route })
         }
         }
     }
     }
 
 
     val visibleItems = navigationItems.filter { isRouteVisible(it.route) }
     val visibleItems = navigationItems.filter { isRouteVisible(it.route) }
     val hiddenItems = navigationItems.filter { !isRouteVisible(it.route) }
     val hiddenItems = navigationItems.filter { !isRouteVisible(it.route) }
-    val selectedDefault = ThemeConfig.defaultHomePage.takeIf { route ->
+    val selectedDefault = settings.defaultHomePage.takeIf { route ->
         visibleItems.any { it.route == route }
         visibleItems.any { it.route == route }
     } ?: MainDestination.Bookshelf.route
     } ?: MainDestination.Bookshelf.route
 
 
@@ -105,7 +93,7 @@ fun MainNavigationSettingsSheet(
                 selectedValue = selectedDefault,
                 selectedValue = selectedDefault,
                 displayEntries = visibleItems.map { stringResource(it.labelId) }.toTypedArray(),
                 displayEntries = visibleItems.map { stringResource(it.labelId) }.toTypedArray(),
                 entryValues = visibleItems.map { it.route }.toTypedArray(),
                 entryValues = visibleItems.map { it.route }.toTypedArray(),
-                onValueChange = { ThemeConfig.defaultHomePage = it },
+                onValueChange = onSetDefault,
             )
             )
             Spacer(modifier = Modifier.padding(bottom = 4.dp))
             Spacer(modifier = Modifier.padding(bottom = 4.dp))
             LazyColumn(
             LazyColumn(
@@ -130,8 +118,7 @@ fun MainNavigationSettingsSheet(
                             navigationItems = navigationItems.toMutableList().apply {
                             navigationItems = navigationItems.toMutableList().apply {
                                 move(indexOf(fromItem), indexOf(toItem))
                                 move(indexOf(fromItem), indexOf(toItem))
                             }
                             }
-                            ThemeConfig.mainNavigationOrder =
-                                navigationItems.joinToString(",") { it.route }
+                            onSetOrder(navigationItems.joinToString(",") { it.route })
                         },
                         },
                         title = stringResource(destination.labelId),
                         title = stringResource(destination.labelId),
                         isEnabled = true,
                         isEnabled = true,

+ 13 - 41
app/src/main/java/io/legado/app/ui/config/themeConfig/NavIconManageSheet.kt

@@ -1,7 +1,5 @@
 package io.legado.app.ui.config.themeConfig
 package io.legado.app.ui.config.themeConfig
 
 
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.result.contract.ActivityResultContracts
 import androidx.annotation.StringRes
 import androidx.annotation.StringRes
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Box
@@ -17,14 +15,9 @@ import androidx.compose.material.icons.filled.Add
 import androidx.compose.material.icons.filled.Close
 import androidx.compose.material.icons.filled.Close
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
 import coil.compose.AsyncImage
 import coil.compose.AsyncImage
@@ -35,55 +28,35 @@ import io.legado.app.ui.widget.components.card.NormalCard
 import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.icon.AppIcon
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.text.AppText
-import java.io.File
+import io.legado.app.domain.model.settings.AppShellSettings
 
 
 private data class NavIconDestination(
 private data class NavIconDestination(
     val key: String,
     val key: String,
     @param:StringRes val labelRes: Int,
     @param:StringRes val labelRes: Int,
     val path: String,
     val path: String,
-    val onSetPath: (String) -> Unit,
 )
 )
 
 
 @OptIn(ExperimentalMaterial3Api::class)
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 @Composable
 fun NavIconManageSheet(
 fun NavIconManageSheet(
     show: Boolean,
     show: Boolean,
-    onDismissRequest: () -> Unit
+    settings: AppShellSettings,
+    onDismissRequest: () -> Unit,
+    onSelectIcon: (String) -> Unit,
+    onClearIcon: (String) -> Unit,
 ) {
 ) {
-    val context = LocalContext.current
-    var activeDest by remember { mutableStateOf<String?>(null) }
-
     val destinations = listOf(
     val destinations = listOf(
         NavIconDestination(
         NavIconDestination(
             "home",
             "home",
             R.string.home,
             R.string.home,
-            ThemeConfig.navIconHome
-        ) { ThemeConfig.navIconHome = it },
-        NavIconDestination("bookshelf", R.string.bookshelf, ThemeConfig.navIconBookshelf) { ThemeConfig.navIconBookshelf = it },
-        NavIconDestination("explore", R.string.discovery, ThemeConfig.navIconExplore) { ThemeConfig.navIconExplore = it },
-        NavIconDestination("rss", R.string.rss, ThemeConfig.navIconRss) { ThemeConfig.navIconRss = it },
-        NavIconDestination("my", R.string.my, ThemeConfig.navIconMy) { ThemeConfig.navIconMy = it },
+            settings.navIconHome
+        ),
+        NavIconDestination("bookshelf", R.string.bookshelf, settings.navIconBookshelf),
+        NavIconDestination("explore", R.string.discovery, settings.navIconExplore),
+        NavIconDestination("rss", R.string.rss, settings.navIconRss),
+        NavIconDestination("my", R.string.my, settings.navIconMy),
     )
     )
 
 
-    val selectImage = rememberLauncherForActivityResult(
-        ActivityResultContracts.GetContent()
-    ) { uri ->
-        val dest = activeDest ?: return@rememberLauncherForActivityResult
-        uri?.let {
-            val inputStream = context.contentResolver.openInputStream(it)
-            val iconDir = File(context.filesDir, "nav_icons")
-            iconDir.mkdirs()
-            val destFile = File(iconDir, "$dest.png")
-            inputStream?.use { input ->
-                destFile.outputStream().use { output ->
-                    input.copyTo(output)
-                }
-            }
-            destinations.firstOrNull { it.key == dest }?.onSetPath?.invoke(destFile.absolutePath)
-        }
-        activeDest = null
-    }
-
     AppModalBottomSheet(
     AppModalBottomSheet(
         show = show,
         show = show,
         onDismissRequest = onDismissRequest,
         onDismissRequest = onDismissRequest,
@@ -106,8 +79,7 @@ fun NavIconManageSheet(
                     ) {
                     ) {
                         NormalCard(
                         NormalCard(
                             onClick = {
                             onClick = {
-                                activeDest = dest.key
-                                selectImage.launch("image/png")
+                                onSelectIcon(dest.key)
                             },
                             },
                             cornerRadius = 12.dp,
                             cornerRadius = 12.dp,
                             containerColor = LegadoTheme.colorScheme.surfaceContainerHigh,
                             containerColor = LegadoTheme.colorScheme.surfaceContainerHigh,
@@ -124,7 +96,7 @@ fun NavIconManageSheet(
                                         contentScale = ContentScale.Fit
                                         contentScale = ContentScale.Fit
                                     )
                                     )
                                     SmallTonalButton(
                                     SmallTonalButton(
-                                        onClick = { dest.onSetPath("") },
+                                        onClick = { onClearIcon(dest.key) },
                                         modifier = Modifier
                                         modifier = Modifier
                                             .align(Alignment.TopEnd)
                                             .align(Alignment.TopEnd)
                                             .padding(4.dp)
                                             .padding(4.dp)

+ 78 - 0
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigContract.kt

@@ -0,0 +1,78 @@
+package io.legado.app.ui.config.themeConfig
+
+import androidx.compose.runtime.Stable
+import io.legado.app.domain.gateway.AppShellSettingsUpdate
+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
+
+@Stable
+data class ThemeConfigUiState(
+    val appShell: AppShellSettings = AppShellSettings(),
+    val theme: ThemeSettings = ThemeSettings(),
+    val fontFolder: String = "",
+    val activeSheet: ThemeConfigSheet? = null,
+)
+
+sealed interface ThemeConfigSheet {
+    data class Background(val dark: Boolean) : ThemeConfigSheet
+    data object NavigationIcons : ThemeConfigSheet
+    data object MainNavigation : ThemeConfigSheet
+    data object LauncherIcon : ThemeConfigSheet
+    data object DividerColor : ThemeConfigSheet
+    data object Font : ThemeConfigSheet
+}
+
+enum class ThemeTimeField {
+    EyeProtectionStart,
+    EyeProtectionEnd,
+}
+
+sealed interface ThemeConfigIntent {
+    data class UpdateAppShell(val update: AppShellSettingsUpdate) : ThemeConfigIntent
+    data class UpdateTheme(val update: ThemeSettingsUpdate) : ThemeConfigIntent
+    data class ShowSheet(val sheet: ThemeConfigSheet) : ThemeConfigIntent
+    data object DismissSheet : ThemeConfigIntent
+    data class SelectTheme(val value: String) : ThemeConfigIntent
+    data class SetMiuixMonet(val enabled: Boolean) : ThemeConfigIntent
+    data class SetDynamicColors(val enabled: Boolean) : ThemeConfigIntent
+    data class SetBlurEnabled(val enabled: Boolean) : ThemeConfigIntent
+    data class SetMainDestinationVisible(
+        val route: String,
+        val visible: Boolean,
+    ) : ThemeConfigIntent
+    data class SetMainNavigationOrder(val routes: String) : ThemeConfigIntent
+    data class SetDefaultHomePage(val route: String) : ThemeConfigIntent
+    data class SelectLauncherIcon(val value: String) : ThemeConfigIntent
+    data class SelectNavigationIcon(val destination: String, val path: String) : ThemeConfigIntent
+    data class RequestNavigationIcon(val destination: String) : ThemeConfigIntent
+    data class RequestBackgroundImage(val dark: Boolean) : ThemeConfigIntent
+    data class SelectBackground(val uri: String, val dark: Boolean) : ThemeConfigIntent
+    data class RemoveBackground(val dark: Boolean) : ThemeConfigIntent
+    data class SelectAppFont(val file: FileDoc) : ThemeConfigIntent
+    data object ClearAppFont : ThemeConfigIntent
+    data class SetFontFolder(val path: String) : ThemeConfigIntent
+    data object RequestFontFolder : ThemeConfigIntent
+    data class RequestTimePicker(
+        val field: ThemeTimeField,
+        val currentValue: String,
+    ) : ThemeConfigIntent
+    data class SetTime(val field: ThemeTimeField, val value: String) : ThemeConfigIntent
+    data object DismissRefactorTip : ThemeConfigIntent
+}
+
+sealed interface ThemeConfigEffect {
+    data object ApplyDayNight : ThemeConfigEffect
+    data object Recreate : ThemeConfigEffect
+    data object NotifyMain : ThemeConfigEffect
+    data class ChangeLauncherIcon(val value: String) : ThemeConfigEffect
+    data object OpenFontFolder : ThemeConfigEffect
+    data class OpenNavigationIcon(val destination: String) : ThemeConfigEffect
+    data class OpenBackgroundImage(val dark: Boolean) : ThemeConfigEffect
+    data class OpenTimePicker(
+        val field: ThemeTimeField,
+        val currentValue: String,
+    ) : ThemeConfigEffect
+    data class ShowToast(val stringRes: Int) : ThemeConfigEffect
+}

+ 130 - 0
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigRouteScreen.kt

@@ -0,0 +1,130 @@
+package io.legado.app.ui.config.themeConfig
+
+import android.content.Intent
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import io.legado.app.constant.EventBus
+import io.legado.app.help.LauncherIconHelp
+import io.legado.app.help.config.ThemeConfigStore
+import io.legado.app.utils.postEvent
+import io.legado.app.utils.takePersistablePermissionSafely
+import io.legado.app.utils.toastOnUi
+import java.io.File
+import java.util.Locale
+import kotlinx.coroutines.flow.collectLatest
+import org.koin.androidx.compose.koinViewModel
+
+@Composable
+fun ThemeConfigRouteScreen(
+    onBackClick: () -> Unit,
+    onNavigateToCustomTheme: () -> Unit,
+    onNavigateToThemeManage: () -> Unit,
+    viewModel: ThemeConfigViewModel = koinViewModel(),
+) {
+    val context = LocalContext.current
+    val state by viewModel.uiState.collectAsStateWithLifecycle()
+    var pendingNavigationDestination by remember { mutableStateOf<String?>(null) }
+    var pendingBackgroundDark by remember { mutableStateOf(false) }
+
+    val fontFolderLauncher = rememberLauncherForActivityResult(
+        ActivityResultContracts.OpenDocumentTree()
+    ) { uri ->
+        if (uri != null) {
+            uri.takePersistablePermissionSafely(context, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+            viewModel.onIntent(ThemeConfigIntent.SetFontFolder(uri.toString()))
+        }
+    }
+    val navigationIconLauncher = rememberLauncherForActivityResult(
+        ActivityResultContracts.GetContent()
+    ) { uri ->
+        val destination = pendingNavigationDestination
+        pendingNavigationDestination = null
+        if (uri != null && destination != null) {
+            runCatching {
+                val iconDir = File(context.filesDir, "nav_icons").apply { mkdirs() }
+                val destinationFile = File(iconDir, "$destination.png")
+                context.contentResolver.openInputStream(uri)?.use { input ->
+                    destinationFile.outputStream().use(input::copyTo)
+                }
+                viewModel.onIntent(
+                    ThemeConfigIntent.SelectNavigationIcon(
+                        destination = destination,
+                        path = destinationFile.absolutePath,
+                    )
+                )
+            }
+        }
+    }
+    val backgroundImageLauncher = rememberLauncherForActivityResult(
+        ActivityResultContracts.GetContent()
+    ) { uri ->
+        if (uri != null) {
+            viewModel.onIntent(
+                ThemeConfigIntent.SelectBackground(uri.toString(), pendingBackgroundDark)
+            )
+        }
+    }
+
+    LaunchedEffect(Unit) {
+        viewModel.effects.collectLatest { effect ->
+            when (effect) {
+                ThemeConfigEffect.ApplyDayNight -> ThemeConfigStore.applyDayNightLive()
+                ThemeConfigEffect.Recreate -> postEvent(EventBus.RECREATE, "")
+                ThemeConfigEffect.NotifyMain -> postEvent(EventBus.NOTIFY_MAIN, true)
+                is ThemeConfigEffect.ChangeLauncherIcon ->
+                    LauncherIconHelp.changeIcon(effect.value)
+                ThemeConfigEffect.OpenFontFolder -> fontFolderLauncher.launch(null)
+                is ThemeConfigEffect.OpenNavigationIcon -> {
+                    pendingNavigationDestination = effect.destination
+                    navigationIconLauncher.launch("image/png")
+                }
+                is ThemeConfigEffect.OpenBackgroundImage -> {
+                    pendingBackgroundDark = effect.dark
+                    backgroundImageLauncher.launch("image/*")
+                }
+                is ThemeConfigEffect.OpenTimePicker -> {
+                    val parts = effect.currentValue.split(":")
+                    val defaultHour = if (effect.field == ThemeTimeField.EyeProtectionStart) 22 else 7
+                    val hour = parts.getOrNull(0)?.toIntOrNull() ?: defaultHour
+                    val minute = parts.getOrNull(1)?.toIntOrNull() ?: 0
+                    android.app.TimePickerDialog(
+                        context,
+                        { _, selectedHour, selectedMinute ->
+                            viewModel.onIntent(
+                                ThemeConfigIntent.SetTime(
+                                    effect.field,
+                                    String.format(
+                                        Locale.US,
+                                        "%02d:%02d",
+                                        selectedHour.coerceIn(0, 23),
+                                        selectedMinute.coerceIn(0, 59),
+                                    ),
+                                )
+                            )
+                        },
+                        hour.coerceIn(0, 23),
+                        minute.coerceIn(0, 59),
+                        true,
+                    ).show()
+                }
+                is ThemeConfigEffect.ShowToast -> context.toastOnUi(effect.stringRes)
+            }
+        }
+    }
+
+    ThemeConfigScreen(
+        state = state,
+        onIntent = viewModel::onIntent,
+        onBackClick = onBackClick,
+        onNavigateToCustomTheme = onNavigateToCustomTheme,
+        onNavigateToThemeManage = onNavigateToThemeManage,
+    )
+}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 338 - 262
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt


+ 259 - 82
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt

@@ -3,104 +3,307 @@ package io.legado.app.ui.config.themeConfig
 import android.net.Uri
 import android.net.Uri
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import androidx.lifecycle.viewModelScope
+import io.legado.app.R
 import io.legado.app.constant.PreferKey
 import io.legado.app.constant.PreferKey
-import io.legado.app.data.local.preferences.LocalPreferencesKeys
-import io.legado.app.data.repository.SettingsRepository
+import io.legado.app.domain.gateway.AppShellBooleanSetting
+import io.legado.app.domain.gateway.AppShellSettingsGateway
+import io.legado.app.domain.gateway.AppShellSettingsUpdate
+import io.legado.app.domain.gateway.AppShellStringSetting
 import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.domain.gateway.ReadSettingsGateway
 import io.legado.app.domain.gateway.ReadSettingsUpdate
 import io.legado.app.domain.gateway.ReadSettingsUpdate
+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.ThemeSettingsGateway
 import io.legado.app.domain.gateway.ThemeSettingsUpdate
 import io.legado.app.domain.gateway.ThemeSettingsUpdate
+import io.legado.app.domain.gateway.ThemeStringSetting
+import io.legado.app.ui.main.MainDestination
 import io.legado.app.utils.FileDoc
 import io.legado.app.utils.FileDoc
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.FileUtils
 import io.legado.app.utils.MD5Utils
 import io.legado.app.utils.MD5Utils
 import io.legado.app.utils.externalFiles
 import io.legado.app.utils.externalFiles
 import io.legado.app.utils.inputStream
 import io.legado.app.utils.inputStream
 import io.legado.app.utils.openInputStream
 import io.legado.app.utils.openInputStream
+import java.io.File
+import java.io.FileOutputStream
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
 import splitties.init.appCtx
 import splitties.init.appCtx
-import java.io.File
-import java.io.FileOutputStream
 
 
 class ThemeConfigViewModel(
 class ThemeConfigViewModel(
-    private val settingsRepository: SettingsRepository,
+    private val appShellSettingsGateway: AppShellSettingsGateway,
     private val readSettingsGateway: ReadSettingsGateway,
     private val readSettingsGateway: ReadSettingsGateway,
     private val themeSettingsGateway: ThemeSettingsGateway,
     private val themeSettingsGateway: ThemeSettingsGateway,
 ) : ViewModel() {
 ) : ViewModel() {
 
 
-    val fontFolder = readSettingsGateway.settings
-        .map { preferences ->
-            val fontFolder: String? = preferences.fontFolder
-            fontFolder
-        }
-        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
+    private val _uiState = MutableStateFlow(
+        ThemeConfigUiState(
+            appShell = appShellSettingsGateway.currentSettings,
+            theme = themeSettingsGateway.currentSettings,
+        )
+    )
+    val uiState = _uiState.asStateFlow()
+
+    private val _effects = MutableSharedFlow<ThemeConfigEffect>(extraBufferCapacity = 16)
+    val effects = _effects.asSharedFlow()
 
 
-    fun setFontFolder(path: String) {
+    init {
         viewModelScope.launch {
         viewModelScope.launch {
-            runCatching {
-                readSettingsGateway.update(ReadSettingsUpdate.FontFolder(path))
+            appShellSettingsGateway.settings.collect { settings ->
+                _uiState.update { it.copy(appShell = settings) }
+            }
+        }
+        viewModelScope.launch {
+            themeSettingsGateway.settings.collect { settings ->
+                _uiState.update { it.copy(theme = settings) }
+            }
+        }
+        viewModelScope.launch {
+            readSettingsGateway.settings.collect { settings ->
+                _uiState.update { it.copy(fontFolder = settings.fontFolder) }
             }
             }
         }
         }
     }
     }
 
 
-    val showThemeRefactorTip = settingsRepository
-        .getPreference(LocalPreferencesKeys.SHOW_THEME_REFACTOR_TIP, true)
-        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true)
+    fun onIntent(intent: ThemeConfigIntent) {
+        when (intent) {
+            is ThemeConfigIntent.UpdateAppShell -> updateAppShell(intent.update)
+            is ThemeConfigIntent.UpdateTheme -> updateTheme(intent.update)
+            is ThemeConfigIntent.ShowSheet ->
+                _uiState.update { it.copy(activeSheet = intent.sheet) }
+            ThemeConfigIntent.DismissSheet ->
+                _uiState.update { it.copy(activeSheet = null) }
+            is ThemeConfigIntent.SelectTheme -> selectTheme(intent.value)
+            is ThemeConfigIntent.SetMiuixMonet -> setMiuixMonet(intent.enabled)
+            is ThemeConfigIntent.SetDynamicColors -> setDynamicColors(intent.enabled)
+            is ThemeConfigIntent.SetBlurEnabled -> setBlurEnabled(intent.enabled)
+            is ThemeConfigIntent.SetMainDestinationVisible -> setMainDestinationVisible(intent)
+            is ThemeConfigIntent.SetMainNavigationOrder -> updateAppShell(
+                AppShellSettingsUpdate.MainNavigationOrder(intent.routes)
+            )
+            is ThemeConfigIntent.SetDefaultHomePage -> updateAppShell(
+                AppShellSettingsUpdate.StringValue(
+                    AppShellStringSetting.DefaultHomePage,
+                    intent.route,
+                )
+            )
+            is ThemeConfigIntent.SelectLauncherIcon -> selectLauncherIcon(intent.value)
+            is ThemeConfigIntent.SelectNavigationIcon -> selectNavigationIcon(intent)
+            is ThemeConfigIntent.RequestNavigationIcon -> _effects.tryEmit(
+                ThemeConfigEffect.OpenNavigationIcon(intent.destination)
+            )
+            is ThemeConfigIntent.RequestBackgroundImage -> _effects.tryEmit(
+                ThemeConfigEffect.OpenBackgroundImage(intent.dark)
+            )
+            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))
+            is ThemeConfigIntent.SetFontFolder -> viewModelScope.launch {
+                readSettingsGateway.update(ReadSettingsUpdate.FontFolder(intent.path))
+            }
+            ThemeConfigIntent.RequestFontFolder -> _effects.tryEmit(ThemeConfigEffect.OpenFontFolder)
+            is ThemeConfigIntent.RequestTimePicker -> _effects.tryEmit(
+                ThemeConfigEffect.OpenTimePicker(intent.field, intent.currentValue)
+            )
+            is ThemeConfigIntent.SetTime -> setTime(intent.field, intent.value)
+            ThemeConfigIntent.DismissRefactorTip -> updateTheme(
+                ThemeSettingsUpdate.BooleanValue(ThemeBooleanSetting.ShowRefactorTip, false)
+            )
+        }
+    }
 
 
-    fun setShowThemeRefactorTip(show: Boolean) {
+    private fun updateAppShell(update: AppShellSettingsUpdate) {
         viewModelScope.launch {
         viewModelScope.launch {
-            settingsRepository.updatePreference(
-                LocalPreferencesKeys.SHOW_THEME_REFACTOR_TIP,
-                show
-            )
+            appShellSettingsGateway.update(update)
+            when (update) {
+                is AppShellSettingsUpdate.ThemeMode ->
+                    _effects.tryEmit(ThemeConfigEffect.ApplyDayNight)
+                is AppShellSettingsUpdate.BooleanValue -> if (
+                    update.setting == AppShellBooleanSetting.ShowStatusBar
+                ) {
+                    _effects.tryEmit(ThemeConfigEffect.NotifyMain)
+                }
+                else -> Unit
+            }
         }
         }
     }
     }
 
 
-    /**
-     * 设置背景图片
-     */
-    suspend fun setBackgroundFromUri(
-        uri: Uri,
-        isDarkTheme: Boolean
-    ) = withContext(Dispatchers.IO) {
-        kotlin.runCatching {
-            val fileDoc = FileDoc.fromUri(uri, false)
-            val suffix = fileDoc.name.substringAfterLast(".", "jpg")
+    private fun updateTheme(update: ThemeSettingsUpdate) {
+        viewModelScope.launch { themeSettingsGateway.update(update) }
+    }
 
 
-            // 计算MD5作为文件名
-            val md5 = uri.inputStream(appCtx).getOrThrow().use {
-                MD5Utils.md5Encode(it)
+    private fun selectTheme(value: String) {
+        val theme = _uiState.value.theme
+        if (value == "13" &&
+            (theme.backgroundImageLight.isNullOrEmpty() || theme.backgroundImageDark.isNullOrEmpty())
+        ) {
+            _effects.tryEmit(ThemeConfigEffect.ShowToast(R.string.transparent_theme_alarm))
+            return
+        }
+        viewModelScope.launch {
+            val updates = buildList {
+                if (value == "13") {
+                    add(ThemeSettingsUpdate.IntValue(ThemeIntSetting.ContainerOpacity, 0))
+                }
+                add(ThemeSettingsUpdate.AppTheme(value))
             }
             }
-            val fileName = "$md5.$suffix"
+            themeSettingsGateway.updateAll(updates)
+            _effects.tryEmit(ThemeConfigEffect.Recreate)
+        }
+    }
+
+    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"))
+                    }
+                }
+            )
+            _effects.tryEmit(ThemeConfigEffect.Recreate)
+        }
+    }
 
 
-            val preferenceKey = if (isDarkTheme) PreferKey.bgImageN else PreferKey.bgImage
-            val folder = File(appCtx.externalFiles, preferenceKey)
-            val file = File(folder, fileName)
+    private fun setDynamicColors(enabled: Boolean) {
+        val value = if (enabled) "0" else "12"
+        if (value != _uiState.value.theme.appTheme) selectTheme(value)
+    }
 
 
-            if (!file.exists()) {
-                FileUtils.createFileIfNotExist(file.absolutePath)
-                uri.inputStream(appCtx).getOrThrow().use { input ->
-                    FileOutputStream(file).use { output ->
-                        input.copyTo(output)
+    private fun setBlurEnabled(enabled: Boolean) {
+        viewModelScope.launch {
+            themeSettingsGateway.updateAll(
+                buildList {
+                    add(
+                        ThemeSettingsUpdate.BooleanValue(
+                            ThemeBooleanSetting.EnableBlur,
+                            enabled,
+                        )
+                    )
+                    if (!enabled) {
+                        add(
+                            ThemeSettingsUpdate.BooleanValue(
+                                ThemeBooleanSetting.EnableProgressiveBlur,
+                                false,
+                            )
+                        )
                     }
                     }
                 }
                 }
-            }
+            )
+        }
+    }
 
 
-            updateBackgroundPath(isDarkTheme, file.absolutePath)
-        }.onFailure {
-            it.printStackTrace()
+    private fun setMainDestinationVisible(intent: ThemeConfigIntent.SetMainDestinationVisible) {
+        val setting = when (intent.route) {
+            MainDestination.Home.route -> AppShellBooleanSetting.ShowHome
+            MainDestination.Explore.route -> AppShellBooleanSetting.ShowDiscovery
+            MainDestination.Rss.route -> AppShellBooleanSetting.ShowRss
+            else -> return
         }
         }
+        viewModelScope.launch {
+            appShellSettingsGateway.updateAll(
+                buildList {
+                    add(AppShellSettingsUpdate.BooleanValue(setting, intent.visible))
+                    if (!intent.visible && _uiState.value.appShell.defaultHomePage == intent.route) {
+                        add(
+                            AppShellSettingsUpdate.StringValue(
+                                AppShellStringSetting.DefaultHomePage,
+                                MainDestination.Bookshelf.route,
+                            )
+                        )
+                    }
+                }
+            )
+        }
+    }
+
+    private fun selectLauncherIcon(value: String) {
+        viewModelScope.launch {
+            appShellSettingsGateway.update(
+                AppShellSettingsUpdate.StringValue(AppShellStringSetting.LauncherIcon, value)
+            )
+            _effects.tryEmit(ThemeConfigEffect.ChangeLauncherIcon(value))
+        }
+    }
+
+    private fun selectNavigationIcon(intent: ThemeConfigIntent.SelectNavigationIcon) {
+        val setting = when (intent.destination) {
+            MainDestination.Home.route -> AppShellStringSetting.NavIconHome
+            MainDestination.Bookshelf.route -> AppShellStringSetting.NavIconBookshelf
+            MainDestination.Explore.route -> AppShellStringSetting.NavIconExplore
+            MainDestination.Rss.route -> AppShellStringSetting.NavIconRss
+            MainDestination.My.route -> AppShellStringSetting.NavIconMy
+            else -> return
+        }
+        updateAppShell(AppShellSettingsUpdate.StringValue(setting, intent.path))
     }
     }
 
 
-    fun removeBackground(isDarkTheme: Boolean) {
-        updateBackgroundPath(isDarkTheme, null)
+    private fun setTime(field: ThemeTimeField, value: String) {
+        val setting = when (field) {
+            ThemeTimeField.EyeProtectionStart -> ThemeStringSetting.EyeProtectionStartTime
+            ThemeTimeField.EyeProtectionEnd -> ThemeStringSetting.EyeProtectionEndTime
+        }
+        updateTheme(ThemeSettingsUpdate.StringValue(setting, value))
     }
     }
 
 
-    fun setAppFont(fileDoc: FileDoc) {
+    private fun setBackground(uriString: String, dark: Boolean) {
+        viewModelScope.launch(Dispatchers.IO) {
+            runCatching {
+                val uri = Uri.parse(uriString)
+                val fileDoc = FileDoc.fromUri(uri, false)
+                val suffix = fileDoc.name.substringAfterLast(".", "jpg")
+                val md5 = uri.inputStream(appCtx).getOrThrow().use(MD5Utils::md5Encode)
+                val preferenceKey = if (dark) PreferKey.bgImageN else PreferKey.bgImage
+                val folder = File(appCtx.externalFiles, preferenceKey)
+                val file = File(folder, "$md5.$suffix")
+                if (!file.exists()) {
+                    FileUtils.createFileIfNotExist(file.absolutePath)
+                    uri.inputStream(appCtx).getOrThrow().use { input ->
+                        FileOutputStream(file).use(input::copyTo)
+                    }
+                }
+                updateBackgroundPath(dark, file.absolutePath)
+            }.onFailure(Throwable::printStackTrace)
+        }
+    }
+
+    private fun removeBackground(dark: Boolean) {
+        viewModelScope.launch(Dispatchers.IO) { updateBackgroundPath(dark, null) }
+    }
+
+    private suspend fun updateBackgroundPath(dark: Boolean, newPath: String?) {
+        val oldPath = if (dark) {
+            _uiState.value.theme.backgroundImageDark
+        } else {
+            _uiState.value.theme.backgroundImageLight
+        }
+        if (oldPath != newPath && oldPath != null) {
+            File(oldPath)
+                .takeIf { it.absolutePath.contains(appCtx.externalFiles.absolutePath) }
+                ?.delete()
+        }
+        themeSettingsGateway.update(
+            ThemeSettingsUpdate.StringValue(
+                if (dark) ThemeStringSetting.BackgroundImageDark
+                else ThemeStringSetting.BackgroundImageLight,
+                newPath,
+            )
+        )
+    }
+
+    private fun setAppFont(fileDoc: FileDoc) {
         viewModelScope.launch(Dispatchers.IO) {
         viewModelScope.launch(Dispatchers.IO) {
             runCatching {
             runCatching {
                 val extension = fileDoc.name.substringAfterLast('.', "ttf")
                 val extension = fileDoc.name.substringAfterLast('.', "ttf")
@@ -113,9 +316,7 @@ class ThemeConfigViewModel(
                 fileDoc.openInputStream().getOrThrow().use { input ->
                 fileDoc.openInputStream().getOrThrow().use { input ->
                     FileOutputStream(temp).use(input::copyTo)
                     FileOutputStream(temp).use(input::copyTo)
                 }
                 }
-                if (target.exists() && !target.delete()) {
-                    error("Unable to replace app font")
-                }
+                if (target.exists() && !target.delete()) error("Unable to replace app font")
                 if (!temp.renameTo(target)) {
                 if (!temp.renameTo(target)) {
                     temp.copyTo(target, overwrite = true)
                     temp.copyTo(target, overwrite = true)
                     temp.delete()
                     temp.delete()
@@ -124,28 +325,4 @@ class ThemeConfigViewModel(
             }.onFailure(Throwable::printStackTrace)
             }.onFailure(Throwable::printStackTrace)
         }
         }
     }
     }
-
-    /**
-     * 更新背景路径并清理旧文件
-     */
-    private fun updateBackgroundPath(isDarkTheme: Boolean, newPath: String?) {
-        val oldPath = if (isDarkTheme) {
-            ThemeConfig.bgImageDark
-        } else {
-            ThemeConfig.bgImageLight
-        }
-
-        if (oldPath != newPath && oldPath != null) {
-            val oldFile = File(oldPath)
-            if (oldFile.absolutePath.contains(appCtx.externalFiles.absolutePath)) {
-                oldFile.delete()
-            }
-        }
-
-        if (isDarkTheme) {
-            ThemeConfig.bgImageDark = newPath
-        } else {
-            ThemeConfig.bgImageLight = newPath
-        }
-    }
 }
 }

+ 101 - 0
app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageRouteScreen.kt

@@ -0,0 +1,101 @@
+package io.legado.app.ui.config.themeManage
+
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import io.legado.app.R
+import io.legado.app.help.config.SavedTheme
+import io.legado.app.help.config.ThemePackageManager
+import io.legado.app.utils.toastOnUi
+import kotlinx.coroutines.flow.collectLatest
+import org.koin.androidx.compose.koinViewModel
+
+@Composable
+fun ThemeManageRouteScreen(
+    onBackClick: () -> Unit,
+    viewModel: ThemeManageViewModel = koinViewModel(),
+) {
+    val context = LocalContext.current
+    val state by viewModel.uiState.collectAsStateWithLifecycle()
+    var exportTarget by remember { mutableStateOf<SavedTheme?>(null) }
+
+    val exportLauncher = rememberLauncherForActivityResult(
+        ActivityResultContracts.CreateDocument("application/zip")
+    ) { uri ->
+        if (uri != null) {
+            val target = exportTarget
+            exportTarget = null
+            viewModel.onIntent(
+                ThemeManageIntent.ExportPackage(
+                    uri = uri.toString(),
+                    themeName = target?.name,
+                    themeData = target?.data,
+                    savedTheme = target,
+                )
+            )
+        }
+    }
+    val importPackageLauncher = rememberLauncherForActivityResult(
+        ActivityResultContracts.OpenDocument()
+    ) { uri ->
+        if (uri != null) viewModel.onIntent(ThemeManageIntent.ImportPackage(uri.toString()))
+    }
+    val importLegacyLauncher = rememberLauncherForActivityResult(
+        ActivityResultContracts.OpenDocument()
+    ) { uri ->
+        if (uri != null) viewModel.onIntent(ThemeManageIntent.ImportLegacyJson(uri.toString()))
+    }
+
+    LaunchedEffect(Unit) {
+        viewModel.effects.collectLatest { effect ->
+            when (effect) {
+                is ThemeManageEffect.OpenExportDocument -> {
+                    exportTarget = effect.theme
+                    val name = effect.theme?.name ?: "materado_theme_${System.currentTimeMillis()}"
+                    exportLauncher.launch("$name.${ThemePackageManager.FILE_EXTENSION}")
+                }
+                ThemeManageEffect.OpenImportPackage -> importPackageLauncher.launch(
+                    arrayOf("application/zip", "application/octet-stream")
+                )
+                ThemeManageEffect.OpenImportLegacyJson -> importLegacyLauncher.launch(
+                    arrayOf("application/json", "text/json")
+                )
+                is ThemeManageEffect.LegacyMigrationFinished -> {
+                    val message = if (effect.failedCount == 0) {
+                        context.getString(R.string.theme_manage_migrate_success, effect.migratedCount)
+                    } else {
+                        context.getString(
+                            R.string.theme_manage_migrate_partial,
+                            effect.migratedCount,
+                            effect.failedCount,
+                        )
+                    }
+                    context.toastOnUi(message)
+                }
+                is ThemeManageEffect.ShowResult -> {
+                    val message = buildString {
+                        append(context.getString(effect.messageRes))
+                        effect.detail?.takeIf(String::isNotBlank)?.let {
+                            append('\n')
+                            append(it)
+                        }
+                    }
+                    context.toastOnUi(message)
+                }
+            }
+        }
+    }
+
+    ThemeManageScreen(
+        state = state,
+        onIntent = viewModel::onIntent,
+        onBackClick = onBackClick,
+    )
+}

+ 47 - 146
app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageScreen.kt

@@ -1,7 +1,5 @@
 package io.legado.app.ui.config.themeManage
 package io.legado.app.ui.config.themeManage
 
 
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.result.contract.ActivityResultContracts
 import androidx.compose.foundation.background
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Box
@@ -24,20 +22,13 @@ import androidx.compose.material.icons.filled.Share
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.shadow
 import androidx.compose.ui.draw.shadow
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.help.config.SavedTheme
 import io.legado.app.help.config.SavedTheme
 import io.legado.app.help.config.ThemePackageManager
 import io.legado.app.help.config.ThemePackageManager
@@ -53,94 +44,17 @@ import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
-import io.legado.app.utils.toastOnUi
-import kotlinx.coroutines.flow.collectLatest
-import org.koin.androidx.compose.koinViewModel
 
 
 @OptIn(ExperimentalMaterial3Api::class)
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 @Composable
 fun ThemeManageScreen(
 fun ThemeManageScreen(
+    state: ThemeManageUiState,
+    onIntent: (ThemeManageIntent) -> Unit,
     onBackClick: () -> Unit,
     onBackClick: () -> Unit,
-    viewModel: ThemeManageViewModel = koinViewModel(),
 ) {
 ) {
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
-    val context = LocalContext.current
-    val state by viewModel.uiState.collectAsStateWithLifecycle()
-
-    var showSaveDialog by remember { mutableStateOf(false) }
-    var newThemeName by remember { mutableStateOf("") }
-    var deleteTarget by remember { mutableStateOf<SavedTheme?>(null) }
-    var applyTarget by remember { mutableStateOf<SavedTheme?>(null) }
-    var exportTarget by remember { mutableStateOf<SavedTheme?>(null) }
-    var editTarget by remember { mutableStateOf<SavedTheme?>(null) }
     val savedThemes = state.savedThemes
     val savedThemes = state.savedThemes
 
 
-    val exportLauncher = rememberLauncherForActivityResult(
-        ActivityResultContracts.CreateDocument("application/zip")
-    ) { uri ->
-        uri?.let {
-            val target = exportTarget
-            exportTarget = null
-            viewModel.onIntent(
-                ThemeManageIntent.ExportPackage(
-                    uri = it.toString(),
-                    themeName = target?.name,
-                    themeData = target?.data,
-                    savedTheme = target,
-                )
-            )
-        }
-    }
-
-    val importPackageLauncher = rememberLauncherForActivityResult(
-        ActivityResultContracts.OpenDocument()
-    ) { uri ->
-        uri?.let {
-            viewModel.onIntent(ThemeManageIntent.ImportPackage(it.toString()))
-        }
-    }
-
-    val importLegacyLauncher = rememberLauncherForActivityResult(
-        ActivityResultContracts.OpenDocument()
-    ) { uri ->
-        uri?.let {
-            viewModel.onIntent(ThemeManageIntent.ImportLegacyJson(it.toString()))
-        }
-    }
-
-    LaunchedEffect(Unit) {
-        viewModel.effects.collectLatest { effect ->
-            when (effect) {
-                is ThemeManageEffect.LegacyMigrationFinished -> {
-                    val message = if (effect.failedCount == 0) {
-                        context.getString(
-                            R.string.theme_manage_migrate_success,
-                            effect.migratedCount,
-                        )
-                    } else {
-                        context.getString(
-                            R.string.theme_manage_migrate_partial,
-                            effect.migratedCount,
-                            effect.failedCount,
-                        )
-                    }
-                    context.toastOnUi(message)
-                }
-
-                is ThemeManageEffect.ShowResult -> {
-                    val message = buildString {
-                        append(context.getString(effect.messageRes))
-                        effect.detail?.takeIf(String::isNotBlank)?.let {
-                            append('\n')
-                            append(it)
-                        }
-                    }
-                    context.toastOnUi(message)
-                }
-            }
-        }
-    }
-
     AppScaffold(
     AppScaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         topBar = {
         topBar = {
@@ -168,37 +82,22 @@ fun ThemeManageScreen(
                     ClickableSettingItem(
                     ClickableSettingItem(
                         title = stringResource(R.string.theme_manage_save_current),
                         title = stringResource(R.string.theme_manage_save_current),
                         description = stringResource(R.string.theme_manage_save_current_summary),
                         description = stringResource(R.string.theme_manage_save_current_summary),
-                        onClick = {
-                            newThemeName = ""
-                            showSaveDialog = true
-                        }
+                        onClick = { onIntent(ThemeManageIntent.OpenSaveDialog) }
                     )
                     )
                     ClickableSettingItem(
                     ClickableSettingItem(
                         title = stringResource(R.string.theme_manage_export_current),
                         title = stringResource(R.string.theme_manage_export_current),
                         description = stringResource(R.string.theme_manage_export_current_summary),
                         description = stringResource(R.string.theme_manage_export_current_summary),
-                        onClick = {
-                            exportTarget = null
-                            exportLauncher.launch(
-                                "materado_theme_${System.currentTimeMillis()}." +
-                                    ThemePackageManager.FILE_EXTENSION
-                            )
-                        }
+                        onClick = { onIntent(ThemeManageIntent.RequestExport()) }
                     )
                     )
                     ClickableSettingItem(
                     ClickableSettingItem(
                         title = stringResource(R.string.theme_manage_import_package),
                         title = stringResource(R.string.theme_manage_import_package),
                         description = stringResource(R.string.theme_manage_import_package_summary),
                         description = stringResource(R.string.theme_manage_import_package_summary),
-                        onClick = {
-                            importPackageLauncher.launch(
-                                arrayOf("application/zip", "application/octet-stream")
-                            )
-                        }
+                        onClick = { onIntent(ThemeManageIntent.RequestImportPackage) }
                     )
                     )
                     ClickableSettingItem(
                     ClickableSettingItem(
                         title = stringResource(R.string.theme_manage_import_config),
                         title = stringResource(R.string.theme_manage_import_config),
                         description = stringResource(R.string.theme_manage_import_config_summary),
                         description = stringResource(R.string.theme_manage_import_config_summary),
-                        onClick = {
-                            importLegacyLauncher.launch(arrayOf("application/json", "text/json"))
-                        }
+                        onClick = { onIntent(ThemeManageIntent.RequestImportLegacyJson) }
                     )
                     )
                     if (state.hasLegacyThemes) {
                     if (state.hasLegacyThemes) {
                         ClickableSettingItem(
                         ClickableSettingItem(
@@ -207,7 +106,7 @@ fun ThemeManageScreen(
                                 R.string.theme_manage_migrate_legacy_summary
                                 R.string.theme_manage_migrate_legacy_summary
                             ),
                             ),
                             onClick = {
                             onClick = {
-                                viewModel.onIntent(ThemeManageIntent.MigrateLegacyThemes)
+                                onIntent(ThemeManageIntent.MigrateLegacyThemes)
                             }
                             }
                         )
                         )
                     }
                     }
@@ -227,15 +126,10 @@ fun ThemeManageScreen(
                 items(savedThemes, key = { it.name }) { theme ->
                 items(savedThemes, key = { it.name }) { theme ->
                     SavedThemeItem(
                     SavedThemeItem(
                         theme = theme,
                         theme = theme,
-                        onApply = { applyTarget = theme },
-                        onEdit = { editTarget = theme },
-                        onExport = {
-                            exportTarget = theme
-                            exportLauncher.launch(
-                                "${theme.name}.${ThemePackageManager.FILE_EXTENSION}"
-                            )
-                        },
-                        onDelete = { deleteTarget = theme }
+                        onApply = { onIntent(ThemeManageIntent.OpenApplyDialog(theme)) },
+                        onEdit = { onIntent(ThemeManageIntent.OpenEditSheet(theme)) },
+                        onExport = { onIntent(ThemeManageIntent.RequestExport(theme)) },
+                        onDelete = { onIntent(ThemeManageIntent.OpenDeleteDialog(theme)) }
                     )
                     )
                 }
                 }
             }
             }
@@ -244,22 +138,23 @@ fun ThemeManageScreen(
 
 
     // Save theme dialog
     // Save theme dialog
     AppAlertDialog(
     AppAlertDialog(
-        show = showSaveDialog,
-        onDismissRequest = { showSaveDialog = false },
+        show = state.dialog is ThemeManageDialog.Save,
+        onDismissRequest = { onIntent(ThemeManageIntent.DismissDialog) },
         title = stringResource(R.string.theme_manage_save_theme),
         title = stringResource(R.string.theme_manage_save_theme),
         confirmText = stringResource(R.string.theme_manage_save),
         confirmText = stringResource(R.string.theme_manage_save),
         onConfirm = {
         onConfirm = {
-            if (newThemeName.isNotBlank()) {
-                viewModel.onIntent(ThemeManageIntent.SaveTheme(newThemeName))
-                showSaveDialog = false
+            val name = (state.dialog as? ThemeManageDialog.Save)?.name.orEmpty()
+            if (name.isNotBlank()) {
+                onIntent(ThemeManageIntent.SaveTheme(name))
+                onIntent(ThemeManageIntent.DismissDialog)
             }
             }
         },
         },
         dismissText = stringResource(R.string.cancel),
         dismissText = stringResource(R.string.cancel),
-        onDismiss = { showSaveDialog = false },
+        onDismiss = { onIntent(ThemeManageIntent.DismissDialog) },
         content = {
         content = {
             AppTextField(
             AppTextField(
-                value = newThemeName,
-                onValueChange = { newThemeName = it },
+                value = (state.dialog as? ThemeManageDialog.Save)?.name.orEmpty(),
+                onValueChange = { onIntent(ThemeManageIntent.UpdateSaveName(it)) },
                 placeholder = { AppText(text = stringResource(R.string.theme_manage_name_hint)) },
                 placeholder = { AppText(text = stringResource(R.string.theme_manage_name_hint)) },
                 singleLine = true,
                 singleLine = true,
                 modifier = Modifier.fillMaxWidth()
                 modifier = Modifier.fillMaxWidth()
@@ -269,53 +164,59 @@ fun ThemeManageScreen(
 
 
     // Apply theme dialog
     // Apply theme dialog
     AppAlertDialog(
     AppAlertDialog(
-        show = applyTarget != null,
-        onDismissRequest = { applyTarget = null },
+        show = state.dialog is ThemeManageDialog.Apply,
+        onDismissRequest = { onIntent(ThemeManageIntent.DismissDialog) },
         title = stringResource(R.string.theme_manage_apply_theme),
         title = stringResource(R.string.theme_manage_apply_theme),
         confirmText = stringResource(R.string.theme_manage_apply),
         confirmText = stringResource(R.string.theme_manage_apply),
         onConfirm = {
         onConfirm = {
-            applyTarget?.let { theme ->
-                viewModel.onIntent(ThemeManageIntent.ApplySavedTheme(theme))
+            (state.dialog as? ThemeManageDialog.Apply)?.theme?.let { theme ->
+                onIntent(ThemeManageIntent.ApplySavedTheme(theme))
             }
             }
-            applyTarget = null
+            onIntent(ThemeManageIntent.DismissDialog)
         },
         },
         dismissText = stringResource(R.string.cancel),
         dismissText = stringResource(R.string.cancel),
-        onDismiss = { applyTarget = null },
-        text = stringResource(R.string.theme_manage_apply_message, applyTarget?.name.orEmpty())
+        onDismiss = { onIntent(ThemeManageIntent.DismissDialog) },
+        text = stringResource(
+            R.string.theme_manage_apply_message,
+            (state.dialog as? ThemeManageDialog.Apply)?.theme?.name.orEmpty(),
+        )
     )
     )
 
 
     // Delete theme dialog
     // Delete theme dialog
     AppAlertDialog(
     AppAlertDialog(
-        show = deleteTarget != null,
-        onDismissRequest = { deleteTarget = null },
+        show = state.dialog is ThemeManageDialog.Delete,
+        onDismissRequest = { onIntent(ThemeManageIntent.DismissDialog) },
         title = stringResource(R.string.theme_manage_delete_theme),
         title = stringResource(R.string.theme_manage_delete_theme),
         confirmText = stringResource(R.string.delete),
         confirmText = stringResource(R.string.delete),
         onConfirm = {
         onConfirm = {
-            deleteTarget?.let { theme ->
-                viewModel.onIntent(ThemeManageIntent.DeleteSavedTheme(theme))
+            (state.dialog as? ThemeManageDialog.Delete)?.theme?.let { theme ->
+                onIntent(ThemeManageIntent.DeleteSavedTheme(theme))
             }
             }
-            deleteTarget = null
+            onIntent(ThemeManageIntent.DismissDialog)
         },
         },
         dismissText = stringResource(R.string.cancel),
         dismissText = stringResource(R.string.cancel),
-        onDismiss = { deleteTarget = null },
-        text = stringResource(R.string.theme_manage_delete_message, deleteTarget?.name.orEmpty())
+        onDismiss = { onIntent(ThemeManageIntent.DismissDialog) },
+        text = stringResource(
+            R.string.theme_manage_delete_message,
+            (state.dialog as? ThemeManageDialog.Delete)?.theme?.name.orEmpty(),
+        )
     )
     )
 
 
     // Edit theme sheet
     // Edit theme sheet
     EditThemeSheet(
     EditThemeSheet(
-        show = editTarget != null,
-        themeData = editTarget?.data,
-        themeName = editTarget?.name ?: "",
-        onDismissRequest = { editTarget = null },
+        show = state.dialog is ThemeManageDialog.Edit,
+        themeData = (state.dialog as? ThemeManageDialog.Edit)?.theme?.data,
+        themeName = (state.dialog as? ThemeManageDialog.Edit)?.theme?.name.orEmpty(),
+        onDismissRequest = { onIntent(ThemeManageIntent.DismissDialog) },
         onSave = { newName, newData ->
         onSave = { newName, newData ->
-            viewModel.onIntent(
+            onIntent(
                 ThemeManageIntent.SaveTheme(
                 ThemeManageIntent.SaveTheme(
                     name = newName,
                     name = newName,
                     data = newData,
                     data = newData,
-                    replacedTheme = editTarget,
+                    replacedTheme = (state.dialog as? ThemeManageDialog.Edit)?.theme,
                 )
                 )
             )
             )
-            editTarget = null
+            onIntent(ThemeManageIntent.DismissDialog)
         }
         }
     )
     )
 }
 }

+ 40 - 1
app/src/main/java/io/legado/app/ui/config/themeManage/ThemeManageViewModel.kt

@@ -37,7 +37,6 @@ class ThemeManageViewModel(
     }
     }
 
 
     fun onIntent(intent: ThemeManageIntent) {
     fun onIntent(intent: ThemeManageIntent) {
-        if (operationMutex.isLocked) return
         when (intent) {
         when (intent) {
             ThemeManageIntent.LoadSavedThemes -> loadSavedThemes()
             ThemeManageIntent.LoadSavedThemes -> loadSavedThemes()
             is ThemeManageIntent.ExportPackage -> exportPackage(intent)
             is ThemeManageIntent.ExportPackage -> exportPackage(intent)
@@ -47,6 +46,26 @@ class ThemeManageViewModel(
             is ThemeManageIntent.ApplySavedTheme -> applySavedTheme(intent.theme)
             is ThemeManageIntent.ApplySavedTheme -> applySavedTheme(intent.theme)
             is ThemeManageIntent.DeleteSavedTheme -> deleteSavedTheme(intent.theme)
             is ThemeManageIntent.DeleteSavedTheme -> deleteSavedTheme(intent.theme)
             ThemeManageIntent.MigrateLegacyThemes -> migrateLegacyThemes()
             ThemeManageIntent.MigrateLegacyThemes -> migrateLegacyThemes()
+            ThemeManageIntent.OpenSaveDialog ->
+                _uiState.update { it.copy(dialog = ThemeManageDialog.Save()) }
+            is ThemeManageIntent.UpdateSaveName -> _uiState.update { state ->
+                val dialog = state.dialog as? ThemeManageDialog.Save ?: return@update state
+                state.copy(dialog = dialog.copy(name = intent.value))
+            }
+            is ThemeManageIntent.OpenApplyDialog ->
+                _uiState.update { it.copy(dialog = ThemeManageDialog.Apply(intent.theme)) }
+            is ThemeManageIntent.OpenDeleteDialog ->
+                _uiState.update { it.copy(dialog = ThemeManageDialog.Delete(intent.theme)) }
+            is ThemeManageIntent.OpenEditSheet ->
+                _uiState.update { it.copy(dialog = ThemeManageDialog.Edit(intent.theme)) }
+            ThemeManageIntent.DismissDialog ->
+                _uiState.update { it.copy(dialog = null) }
+            is ThemeManageIntent.RequestExport ->
+                _effects.tryEmit(ThemeManageEffect.OpenExportDocument(intent.theme))
+            ThemeManageIntent.RequestImportPackage ->
+                _effects.tryEmit(ThemeManageEffect.OpenImportPackage)
+            ThemeManageIntent.RequestImportLegacyJson ->
+                _effects.tryEmit(ThemeManageEffect.OpenImportLegacyJson)
         }
         }
     }
     }
 
 
@@ -222,8 +241,16 @@ data class ThemeManageUiState(
     val loading: Boolean = false,
     val loading: Boolean = false,
     val savedThemes: ImmutableList<SavedTheme> = persistentListOf(),
     val savedThemes: ImmutableList<SavedTheme> = persistentListOf(),
     val hasLegacyThemes: Boolean = false,
     val hasLegacyThemes: Boolean = false,
+    val dialog: ThemeManageDialog? = null,
 )
 )
 
 
+sealed interface ThemeManageDialog {
+    data class Save(val name: String = "") : ThemeManageDialog
+    data class Apply(val theme: SavedTheme) : ThemeManageDialog
+    data class Delete(val theme: SavedTheme) : ThemeManageDialog
+    data class Edit(val theme: SavedTheme) : ThemeManageDialog
+}
+
 sealed interface ThemeManageIntent {
 sealed interface ThemeManageIntent {
     data object LoadSavedThemes : ThemeManageIntent
     data object LoadSavedThemes : ThemeManageIntent
 
 
@@ -245,9 +272,21 @@ sealed interface ThemeManageIntent {
     data class ApplySavedTheme(val theme: SavedTheme) : ThemeManageIntent
     data class ApplySavedTheme(val theme: SavedTheme) : ThemeManageIntent
     data class DeleteSavedTheme(val theme: SavedTheme) : ThemeManageIntent
     data class DeleteSavedTheme(val theme: SavedTheme) : ThemeManageIntent
     data object MigrateLegacyThemes : ThemeManageIntent
     data object MigrateLegacyThemes : ThemeManageIntent
+    data object OpenSaveDialog : ThemeManageIntent
+    data class UpdateSaveName(val value: String) : ThemeManageIntent
+    data class OpenApplyDialog(val theme: SavedTheme) : ThemeManageIntent
+    data class OpenDeleteDialog(val theme: SavedTheme) : ThemeManageIntent
+    data class OpenEditSheet(val theme: SavedTheme) : ThemeManageIntent
+    data object DismissDialog : ThemeManageIntent
+    data class RequestExport(val theme: SavedTheme? = null) : ThemeManageIntent
+    data object RequestImportPackage : ThemeManageIntent
+    data object RequestImportLegacyJson : ThemeManageIntent
 }
 }
 
 
 sealed interface ThemeManageEffect {
 sealed interface ThemeManageEffect {
+    data class OpenExportDocument(val theme: SavedTheme?) : ThemeManageEffect
+    data object OpenImportPackage : ThemeManageEffect
+    data object OpenImportLegacyJson : ThemeManageEffect
     data class LegacyMigrationFinished(
     data class LegacyMigrationFinished(
         val migratedCount: Int,
         val migratedCount: Int,
         val failedCount: Int,
         val failedCount: Int,

+ 17 - 0
app/src/main/java/io/legado/app/ui/config/translation/TranslationConfigContract.kt

@@ -0,0 +1,17 @@
+package io.legado.app.ui.config.translation
+
+import androidx.compose.runtime.Stable
+import io.legado.app.domain.model.settings.TranslationSettings
+
+@Stable
+data class TranslationConfigUiState(
+    val settings: TranslationSettings = TranslationSettings(),
+)
+
+sealed interface TranslationConfigIntent {
+    data class SetProvider(val value: String) : TranslationConfigIntent
+    data class SetTargetLanguage(val value: String) : TranslationConfigIntent
+    data class SetMaxCharsPerChunk(val value: Int) : TranslationConfigIntent
+}
+
+sealed interface TranslationConfigEffect

+ 39 - 27
app/src/main/java/io/legado/app/ui/config/translation/TranslationConfigScreen.kt

@@ -1,6 +1,5 @@
 package io.legado.app.ui.config.translation
 package io.legado.app.ui.config.translation
 
 
-import android.annotation.SuppressLint
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material3.ExperimentalMaterial3Api
 import androidx.compose.material3.ExperimentalMaterial3Api
@@ -9,7 +8,9 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.R
+import io.legado.app.domain.model.TranslationConstants
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.theme.adaptiveContentPadding
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.AppScaffold
 import io.legado.app.ui.widget.components.SplicedColumnGroup
 import io.legado.app.ui.widget.components.SplicedColumnGroup
@@ -19,77 +20,88 @@ import io.legado.app.ui.widget.components.settingItem.SliderSettingItem
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
+import org.koin.androidx.compose.koinViewModel
+
+@Composable
+fun TranslationConfigRouteScreen(
+    onBackClick: () -> Unit,
+    onNavigateToAi: () -> Unit,
+    viewModel: TranslationConfigViewModel = koinViewModel(),
+) {
+    TranslationConfigScreen(
+        state = viewModel.uiState.collectAsStateWithLifecycle().value,
+        onIntent = viewModel::onIntent,
+        onBackClick = onBackClick,
+        onNavigateToAi = onNavigateToAi,
+    )
+}
 
 
-@SuppressLint("RememberReturnType")
 @OptIn(ExperimentalMaterial3Api::class)
 @OptIn(ExperimentalMaterial3Api::class)
 @Composable
 @Composable
 fun TranslationConfigScreen(
 fun TranslationConfigScreen(
+    state: TranslationConfigUiState,
+    onIntent: (TranslationConfigIntent) -> Unit,
     onBackClick: () -> Unit,
     onBackClick: () -> Unit,
-    onNavigateToAi: () -> Unit
+    onNavigateToAi: () -> Unit,
 ) {
 ) {
+    val settings = state.settings
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
-
     AppScaffold(
     AppScaffold(
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
         topBar = {
         topBar = {
             GlassMediumFlexibleTopAppBar(
             GlassMediumFlexibleTopAppBar(
                 title = stringResource(R.string.translation_config),
                 title = stringResource(R.string.translation_config),
                 scrollBehavior = scrollBehavior,
                 scrollBehavior = scrollBehavior,
-                navigationIcon = {
-                    TopBarNavigationButton(onClick = onBackClick)
-                }
+                navigationIcon = { TopBarNavigationButton(onClick = onBackClick) },
             )
             )
-        }
+        },
     ) { paddingValues ->
     ) { paddingValues ->
         LazyColumn(
         LazyColumn(
             modifier = Modifier.fillMaxSize(),
             modifier = Modifier.fillMaxSize(),
             contentPadding = adaptiveContentPadding(
             contentPadding = adaptiveContentPadding(
                 top = paddingValues.calculateTopPadding(),
                 top = paddingValues.calculateTopPadding(),
-                bottom = 120.dp
-            )
+                bottom = 120.dp,
+            ),
         ) {
         ) {
             item {
             item {
                 SplicedColumnGroup(title = stringResource(R.string.translation_provider)) {
                 SplicedColumnGroup(title = stringResource(R.string.translation_provider)) {
                     DropdownListSettingItem(
                     DropdownListSettingItem(
                         title = stringResource(R.string.llm_provider),
                         title = stringResource(R.string.llm_provider),
-                        selectedValue = TranslationConfig.llmProvider,
-                        displayEntries = TranslationConfig.providerDisplayNames.toTypedArray(),
-                        entryValues = TranslationConfig.providerValues.toTypedArray(),
-                        onValueChange = { TranslationConfig.llmProvider = it }
+                        selectedValue = settings.provider,
+                        displayEntries = TranslationConstants.providerDisplayNames.toTypedArray(),
+                        entryValues = TranslationConstants.providerValues.toTypedArray(),
+                        onValueChange = { onIntent(TranslationConfigIntent.SetProvider(it)) },
                     )
                     )
                 }
                 }
             }
             }
-
             item {
             item {
                 SplicedColumnGroup(title = stringResource(R.string.translation_options)) {
                 SplicedColumnGroup(title = stringResource(R.string.translation_options)) {
-                    val languageEntries = TranslationConfig.targetLanguages.map { it.second }.toTypedArray()
-                    val languageValues = TranslationConfig.targetLanguages.map { it.first }.toTypedArray()
                     DropdownListSettingItem(
                     DropdownListSettingItem(
                         title = stringResource(R.string.llm_target_language),
                         title = stringResource(R.string.llm_target_language),
-                        selectedValue = TranslationConfig.llmTargetLanguage,
-                        displayEntries = languageEntries,
-                        entryValues = languageValues,
-                        onValueChange = { TranslationConfig.llmTargetLanguage = it }
+                        selectedValue = settings.targetLanguage,
+                        displayEntries = TranslationConstants.targetLanguages.map { it.second }.toTypedArray(),
+                        entryValues = TranslationConstants.targetLanguages.map { it.first }.toTypedArray(),
+                        onValueChange = { onIntent(TranslationConfigIntent.SetTargetLanguage(it)) },
                     )
                     )
-
                     SliderSettingItem(
                     SliderSettingItem(
                         title = stringResource(R.string.llm_max_chars_per_chunk),
                         title = stringResource(R.string.llm_max_chars_per_chunk),
-                        value = TranslationConfig.llmMaxCharsPerChunk.toFloat(),
+                        value = settings.maxCharsPerChunk.toFloat(),
                         defaultValue = 10000f,
                         defaultValue = 10000f,
                         valueRange = 1000f..10000f,
                         valueRange = 1000f..10000f,
                         steps = 17,
                         steps = 17,
-                        onValueChange = { TranslationConfig.llmMaxCharsPerChunk = it.toInt() }
+                        onValueChange = {
+                            onIntent(TranslationConfigIntent.SetMaxCharsPerChunk(it.toInt()))
+                        },
                     )
                     )
                 }
                 }
             }
             }
-
-            if (TranslationConfig.llmProvider == TranslationConfig.PROVIDER_APP_AI) {
+            if (settings.provider == TranslationConstants.PROVIDER_APP_AI) {
                 item {
                 item {
                     SplicedColumnGroup(title = stringResource(R.string.ai_config)) {
                     SplicedColumnGroup(title = stringResource(R.string.ai_config)) {
                         ClickableSettingItem(
                         ClickableSettingItem(
                             title = stringResource(R.string.translation_app_ai_provider),
                             title = stringResource(R.string.translation_app_ai_provider),
                             description = stringResource(R.string.translation_app_ai_provider_summary),
                             description = stringResource(R.string.translation_app_ai_provider_summary),
-                            onClick = onNavigateToAi
+                            onClick = onNavigateToAi,
                         )
                         )
                     }
                     }
                 }
                 }

+ 40 - 0
app/src/main/java/io/legado/app/ui/config/translation/TranslationConfigViewModel.kt

@@ -0,0 +1,40 @@
+package io.legado.app.ui.config.translation
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import io.legado.app.domain.gateway.TranslationSettingsGateway
+import io.legado.app.domain.gateway.TranslationSettingsUpdate
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+
+class TranslationConfigViewModel(
+    private val settingsGateway: TranslationSettingsGateway,
+) : ViewModel() {
+    private val _uiState = MutableStateFlow(TranslationConfigUiState())
+    val uiState = _uiState.asStateFlow()
+
+    private val _effects = MutableSharedFlow<TranslationConfigEffect>(extraBufferCapacity = 16)
+    val effects = _effects.asSharedFlow()
+
+    init {
+        viewModelScope.launch {
+            settingsGateway.settings.collect { settings ->
+                _uiState.update { it.copy(settings = settings) }
+            }
+        }
+    }
+
+    fun onIntent(intent: TranslationConfigIntent) {
+        val update = when (intent) {
+            is TranslationConfigIntent.SetProvider -> TranslationSettingsUpdate.Provider(intent.value)
+            is TranslationConfigIntent.SetTargetLanguage -> TranslationSettingsUpdate.TargetLanguage(intent.value)
+            is TranslationConfigIntent.SetMaxCharsPerChunk ->
+                TranslationSettingsUpdate.MaxCharsPerChunk(intent.value)
+        }
+        viewModelScope.launch { settingsGateway.update(update) }
+    }
+}

+ 12 - 12
app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt

@@ -53,15 +53,15 @@ import io.legado.app.ui.config.ai.AiProviderEditRouteScreen
 import io.legado.app.ui.config.ai.summary.AiSummaryConfigRouteScreen
 import io.legado.app.ui.config.ai.summary.AiSummaryConfigRouteScreen
 import io.legado.app.ui.config.backupConfig.BackupConfigScreen
 import io.legado.app.ui.config.backupConfig.BackupConfigScreen
 import io.legado.app.ui.config.coverConfig.CoverAlbumManageRouteScreen
 import io.legado.app.ui.config.coverConfig.CoverAlbumManageRouteScreen
-import io.legado.app.ui.config.coverConfig.CoverConfigScreen
+import io.legado.app.ui.config.coverConfig.CoverConfigRouteScreen
 import io.legado.app.ui.config.customTheme.CustomThemeRouteScreen
 import io.legado.app.ui.config.customTheme.CustomThemeRouteScreen
-import io.legado.app.ui.config.downloadCacheConfig.DownloadCacheConfigScreen
-import io.legado.app.ui.config.labConfig.LabConfigScreen
+import io.legado.app.ui.config.downloadCacheConfig.DownloadCacheConfigRouteScreen
+import io.legado.app.ui.config.labConfig.LabConfigRouteScreen
 import io.legado.app.ui.config.otherConfig.OtherConfigRouteScreen
 import io.legado.app.ui.config.otherConfig.OtherConfigRouteScreen
 import io.legado.app.ui.config.readConfig.ReadConfigRouteScreen
 import io.legado.app.ui.config.readConfig.ReadConfigRouteScreen
-import io.legado.app.ui.config.themeConfig.ThemeConfigScreen
-import io.legado.app.ui.config.themeManage.ThemeManageScreen
-import io.legado.app.ui.config.translation.TranslationConfigScreen
+import io.legado.app.ui.config.themeConfig.ThemeConfigRouteScreen
+import io.legado.app.ui.config.themeManage.ThemeManageRouteScreen
+import io.legado.app.ui.config.translation.TranslationConfigRouteScreen
 import io.legado.app.ui.highlightTagRule.HighlightTagRuleScreen
 import io.legado.app.ui.highlightTagRule.HighlightTagRuleScreen
 import io.legado.app.ui.rss.article.MainRouteRssSort
 import io.legado.app.ui.rss.article.MainRouteRssSort
 import io.legado.app.ui.rss.article.RssSortRouteScreen
 import io.legado.app.ui.rss.article.RssSortRouteScreen
@@ -210,7 +210,7 @@ fun MainActivity.mainEntryProvider(
     }
     }
 
 
     entry<MainRouteSettingsCover> {
     entry<MainRouteSettingsCover> {
-        CoverConfigScreen(
+        CoverConfigRouteScreen(
             onBackClick = { onNavigateBack() },
             onBackClick = { onNavigateBack() },
             onNavigateToCoverAlbums = {
             onNavigateToCoverAlbums = {
                 backStack.add(MainRouteSettingsCoverAlbums)
                 backStack.add(MainRouteSettingsCoverAlbums)
@@ -223,7 +223,7 @@ fun MainActivity.mainEntryProvider(
     }
     }
 
 
     entry<MainRouteSettingsTheme> {
     entry<MainRouteSettingsTheme> {
-        ThemeConfigScreen(
+        ThemeConfigRouteScreen(
             onBackClick = { onNavigateBack() },
             onBackClick = { onNavigateBack() },
             onNavigateToCustomTheme = { backStack.add(MainRouteSettingsCustomTheme) },
             onNavigateToCustomTheme = { backStack.add(MainRouteSettingsCustomTheme) },
             onNavigateToThemeManage = { backStack.add(MainRouteSettingsThemeManage) }
             onNavigateToThemeManage = { backStack.add(MainRouteSettingsThemeManage) }
@@ -290,18 +290,18 @@ fun MainActivity.mainEntryProvider(
     }
     }
 
 
     entry<MainRouteSettingsDownloadCache> {
     entry<MainRouteSettingsDownloadCache> {
-        DownloadCacheConfigScreen(onBackClick = { onNavigateBack() })
+        DownloadCacheConfigRouteScreen(onBackClick = { onNavigateBack() })
     }
     }
 
 
     entry<MainRouteSettingsTranslation> {
     entry<MainRouteSettingsTranslation> {
-        TranslationConfigScreen(
+        TranslationConfigRouteScreen(
             onBackClick = { onNavigateBack() },
             onBackClick = { onNavigateBack() },
             onNavigateToAi = { backStack.add(MainRouteSettingsAi) }
             onNavigateToAi = { backStack.add(MainRouteSettingsAi) }
         )
         )
     }
     }
 
 
     entry<MainRouteSettingsLabConfig> {
     entry<MainRouteSettingsLabConfig> {
-        LabConfigScreen(onBackClick = { onNavigateBack() })
+        LabConfigRouteScreen(onBackClick = { onNavigateBack() })
     }
     }
 
 
     entry<MainRouteSettingsCustomTheme> {
     entry<MainRouteSettingsCustomTheme> {
@@ -311,7 +311,7 @@ fun MainActivity.mainEntryProvider(
     }
     }
 
 
     entry<MainRouteSettingsThemeManage> {
     entry<MainRouteSettingsThemeManage> {
-        ThemeManageScreen(onBackClick = { onNavigateBack() })
+        ThemeManageRouteScreen(onBackClick = { onNavigateBack() })
     }
     }
 
 
     entry<MainRouteImportLocal> {
     entry<MainRouteImportLocal> {

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor