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

fix: 避免配置切换重建并收口 UDF 状态

5151561 3 месяцев назад
Родитель
Сommit
1ba32fcfc3
23 измененных файлов с 203 добавлено и 91 удалено
  1. 9 9
      app/src/main/AndroidManifest.xml
  2. 9 1
      app/src/main/java/io/legado/app/App.kt
  3. 4 3
      app/src/main/java/io/legado/app/base/BaseComposeActivity.kt
  4. 1 0
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  5. 2 2
      app/src/main/java/io/legado/app/data/repository/FeatureSettingsRepositories.kt
  6. 1 1
      app/src/main/java/io/legado/app/di/appModule.kt
  7. 3 2
      app/src/main/java/io/legado/app/help/config/LocalConfig.kt
  8. 0 10
      app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt
  9. 0 3
      app/src/main/java/io/legado/app/ui/config/customTheme/CustomThemeRouteScreen.kt
  10. 1 1
      app/src/main/java/io/legado/app/ui/config/mainConfig/MainConfig.kt
  11. 11 10
      app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt
  12. 6 16
      app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt
  13. 0 1
      app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigContract.kt
  14. 0 1
      app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigRouteScreen.kt
  15. 0 2
      app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt
  16. 15 0
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
  17. 2 0
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt
  18. 19 16
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  19. 16 3
      app/src/main/java/io/legado/app/ui/theme/AppTheme.kt
  20. 1 2
      app/src/main/java/io/legado/app/ui/theme/CustomColorScheme.kt
  21. 19 6
      app/src/main/java/io/legado/app/ui/theme/ThemeEngine.kt
  22. 2 2
      app/src/main/java/io/legado/app/ui/theme/ThemeResolver.kt
  23. 82 0
      app/src/test/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModelTest.kt

+ 9 - 9
app/src/main/AndroidManifest.xml

@@ -39,7 +39,7 @@
         <activity
         <activity
             android:name=".ui.main.LauncherW"
             android:name=".ui.main.LauncherW"
             android:enabled="false"
             android:enabled="false"
-            android:configChanges="uiMode"
+            android:configChanges="locale|layoutDirection|uiMode"
             android:exported="true"
             android:exported="true"
             android:icon="@mipmap/launcherw">
             android:icon="@mipmap/launcherw">
             <intent-filter>
             <intent-filter>
@@ -55,7 +55,7 @@
         <activity
         <activity
             android:name=".ui.main.Launcher0"
             android:name=".ui.main.Launcher0"
             android:enabled="false"
             android:enabled="false"
-            android:configChanges="uiMode"
+            android:configChanges="locale|layoutDirection|uiMode"
             android:exported="true"
             android:exported="true"
             android:icon="@mipmap/launcher0">
             android:icon="@mipmap/launcher0">
             <intent-filter>
             <intent-filter>
@@ -72,7 +72,7 @@
         <activity
         <activity
             android:name=".ui.main.Launcher1"
             android:name=".ui.main.Launcher1"
             android:enabled="false"
             android:enabled="false"
-            android:configChanges="uiMode"
+            android:configChanges="locale|layoutDirection|uiMode"
             android:exported="true"
             android:exported="true"
             android:icon="@mipmap/launcher1">
             android:icon="@mipmap/launcher1">
             <intent-filter>
             <intent-filter>
@@ -89,7 +89,7 @@
         <activity
         <activity
             android:name=".ui.main.Launcher2"
             android:name=".ui.main.Launcher2"
             android:enabled="false"
             android:enabled="false"
-            android:configChanges="uiMode"
+            android:configChanges="locale|layoutDirection|uiMode"
             android:exported="true"
             android:exported="true"
             android:icon="@mipmap/launcher2">
             android:icon="@mipmap/launcher2">
             <intent-filter>
             <intent-filter>
@@ -106,7 +106,7 @@
         <activity
         <activity
             android:name=".ui.main.Launcher3"
             android:name=".ui.main.Launcher3"
             android:enabled="false"
             android:enabled="false"
-            android:configChanges="uiMode"
+            android:configChanges="locale|layoutDirection|uiMode"
             android:exported="true"
             android:exported="true"
             android:icon="@mipmap/launcher3">
             android:icon="@mipmap/launcher3">
             <intent-filter>
             <intent-filter>
@@ -123,7 +123,7 @@
         <activity
         <activity
             android:name=".ui.main.Launcher4"
             android:name=".ui.main.Launcher4"
             android:enabled="false"
             android:enabled="false"
-            android:configChanges="uiMode"
+            android:configChanges="locale|layoutDirection|uiMode"
             android:exported="true"
             android:exported="true"
             android:icon="@mipmap/launcher4">
             android:icon="@mipmap/launcher4">
             <intent-filter>
             <intent-filter>
@@ -140,7 +140,7 @@
         <activity
         <activity
             android:name=".ui.main.Launcher5"
             android:name=".ui.main.Launcher5"
             android:enabled="false"
             android:enabled="false"
-            android:configChanges="uiMode"
+            android:configChanges="locale|layoutDirection|uiMode"
             android:exported="true"
             android:exported="true"
             android:icon="@mipmap/launcher5">
             android:icon="@mipmap/launcher5">
             <intent-filter>
             <intent-filter>
@@ -157,7 +157,7 @@
         <activity
         <activity
             android:name=".ui.main.Launcher6"
             android:name=".ui.main.Launcher6"
             android:enabled="false"
             android:enabled="false"
-            android:configChanges="uiMode"
+            android:configChanges="locale|layoutDirection|uiMode"
             android:exported="true"
             android:exported="true"
             android:icon="@mipmap/launcher6">
             android:icon="@mipmap/launcher6">
             <intent-filter>
             <intent-filter>
@@ -174,7 +174,7 @@
         <activity
         <activity
             android:name=".ui.main.MainActivity"
             android:name=".ui.main.MainActivity"
             android:alwaysRetainTaskState="true"
             android:alwaysRetainTaskState="true"
-            android:configChanges="uiMode"
+            android:configChanges="locale|layoutDirection|uiMode"
             android:enableOnBackInvokedCallback="true"
             android:enableOnBackInvokedCallback="true"
             android:windowSoftInputMode="adjustResize"
             android:windowSoftInputMode="adjustResize"
             android:exported="true">
             android:exported="true">

+ 9 - 1
app/src/main/java/io/legado/app/App.kt

@@ -37,6 +37,7 @@ import io.legado.app.data.entities.rule.ExploreRule
 import io.legado.app.data.entities.rule.SearchRule
 import io.legado.app.data.entities.rule.SearchRule
 import io.legado.app.di.appDatabaseModule
 import io.legado.app.di.appDatabaseModule
 import io.legado.app.di.appModule
 import io.legado.app.di.appModule
+import io.legado.app.domain.gateway.BackupSettingsGateway
 import io.legado.app.help.AppFreezeMonitor
 import io.legado.app.help.AppFreezeMonitor
 import io.legado.app.help.AppWebDav
 import io.legado.app.help.AppWebDav
 import io.legado.app.help.CrashHandler
 import io.legado.app.help.CrashHandler
@@ -70,6 +71,8 @@ import io.legado.app.utils.getPrefBoolean
 import io.legado.app.utils.getPrefString
 import io.legado.app.utils.getPrefString
 import io.legado.app.utils.isDebuggable
 import io.legado.app.utils.isDebuggable
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
 import org.chromium.base.ThreadUtils
 import org.chromium.base.ThreadUtils
 import org.koin.android.ext.android.get
 import org.koin.android.ext.android.get
 import org.koin.android.ext.koin.androidContext
 import org.koin.android.ext.koin.androidContext
@@ -150,7 +153,12 @@ class App : Application(), ImageLoaderFactory {
         oldConfig = Configuration(resources.configuration)
         oldConfig = Configuration(resources.configuration)
         registerActivityLifecycleCallbacks(LifecycleHelp)
         registerActivityLifecycleCallbacks(LifecycleHelp)
         Coroutine.async {
         Coroutine.async {
-            AppWebDav.upConfig()
+            get<BackupSettingsGateway>().settings
+                .map {
+                    listOf(it.webDavUrl, it.webDavDir, it.webDavAccount, it.webDavPassword)
+                }
+                .distinctUntilChanged()
+                .collect { AppWebDav.upConfig() }
         }
         }
         Coroutine.async {
         Coroutine.async {
             LogUtils.init(this@App)
             LogUtils.init(this@App)

+ 4 - 3
app/src/main/java/io/legado/app/base/BaseComposeActivity.kt

@@ -65,9 +65,10 @@ abstract class BaseComposeActivity(
 
 
     override fun onConfigurationChanged(newConfig: Configuration) {
     override fun onConfigurationChanged(newConfig: Configuration) {
         super.onConfigurationChanged(newConfig)
         super.onConfigurationChanged(newConfig)
-        // manifest 声明 uiMode configChanges 后,切换深浅色不再 recreate,
-        // 而 AppCompat 手动回调本方法时不会走 View 树分发,需要自己同步给
-        // Compose(LocalConfiguration)并刷新系统栏与背景图
+        // manifest 声明 locale/layoutDirection/uiMode configChanges 后,Compose 主壳
+        // 可直接响应语言与深浅色变化,无需销毁 Activity。AppCompat 手动回调本方法时
+        // 不会走 View 树分发,需要自己同步给 Compose(LocalConfiguration),并刷新
+        // 系统栏与背景图。
         window.decorView.dispatchConfigurationChanged(newConfig)
         window.decorView.dispatchConfigurationChanged(newConfig)
         setupSystemBar()
         setupSystemBar()
         if (imageBg) {
         if (imageBg) {

+ 1 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -49,6 +49,7 @@ object PreferKey {
     const val enableReview = "enableReview"
     const val enableReview = "enableReview"
     const val showRss = "showRss"
     const val showRss = "showRss"
     const val mainNavigationOrder = "mainNavigationOrder"
     const val mainNavigationOrder = "mainNavigationOrder"
+    const val navExtended = "navExtended"
     const val showStatusBar = "showStatusBar"
     const val showStatusBar = "showStatusBar"
     const val swipeAnimation = "swipeAnimation"
     const val swipeAnimation = "swipeAnimation"
     const val bookshelfLayout = "bookshelfLayout"
     const val bookshelfLayout = "bookshelfLayout"

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

@@ -73,7 +73,7 @@ class AppShellSettingsRepository : AppShellSettingsGateway {
                 AppShellBooleanSetting.UseFloatingBottomBar -> PreferKey.useFloatingBottomBar to update.value
                 AppShellBooleanSetting.UseFloatingBottomBar -> PreferKey.useFloatingBottomBar to update.value
                 AppShellBooleanSetting.UseFloatingBottomBarLiquidGlass ->
                 AppShellBooleanSetting.UseFloatingBottomBarLiquidGlass ->
                     PreferKey.useFloatingBottomBarLiquidGlass to update.value
                     PreferKey.useFloatingBottomBarLiquidGlass to update.value
-                AppShellBooleanSetting.NavExtended -> "navExtended" to update.value
+                AppShellBooleanSetting.NavExtended -> PreferKey.navExtended to update.value
             }
             }
             is AppShellSettingsUpdate.StringValue -> when (update.setting) {
             is AppShellSettingsUpdate.StringValue -> when (update.setting) {
                 AppShellStringSetting.TabletInterface -> PreferKey.tabletInterface to update.value
                 AppShellStringSetting.TabletInterface -> PreferKey.tabletInterface to update.value
@@ -382,7 +382,7 @@ private fun Preferences.toAppShellSettings(): AppShellSettings = AppShellSetting
     defaultHomePage = compatDsString(PreferKey.defaultHomePage) ?: "bookshelf",
     defaultHomePage = compatDsString(PreferKey.defaultHomePage) ?: "bookshelf",
     mainNavigationOrder = compatDsString(PreferKey.mainNavigationOrder)
     mainNavigationOrder = compatDsString(PreferKey.mainNavigationOrder)
         ?: "home,bookshelf,explore,rss,my",
         ?: "home,bookshelf,explore,rss,my",
-    navExtended = compatDsBoolean("navExtended") ?: false,
+    navExtended = compatDsBoolean(PreferKey.navExtended) ?: false,
     navIconHome = compatDsString(PreferKey.navIconHome) ?: "",
     navIconHome = compatDsString(PreferKey.navIconHome) ?: "",
     navIconBookshelf = compatDsString(PreferKey.navIconBookshelf) ?: "",
     navIconBookshelf = compatDsString(PreferKey.navIconBookshelf) ?: "",
     navIconExplore = compatDsString(PreferKey.navIconExplore) ?: "",
     navIconExplore = compatDsString(PreferKey.navIconExplore) ?: "",

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

@@ -366,7 +366,7 @@ val appModule = module {
     viewModelOf(::AllBookmarkViewModel)
     viewModelOf(::AllBookmarkViewModel)
     viewModelOf(::TxtTocRuleViewModel)
     viewModelOf(::TxtTocRuleViewModel)
     viewModel { TxtTocRulePreviewViewModel(app = get(), repository = get()) }
     viewModel { TxtTocRulePreviewViewModel(app = get(), repository = get()) }
-    viewModelOf(::OtherConfigViewModel)
+    viewModel { OtherConfigViewModel(get(), get()) }
     viewModelOf(::CustomThemeViewModel)
     viewModelOf(::CustomThemeViewModel)
     viewModelOf(::ReadConfigViewModel)
     viewModelOf(::ReadConfigViewModel)
     viewModelOf(::CoverConfigViewModel)
     viewModelOf(::CoverConfigViewModel)

+ 3 - 2
app/src/main/java/io/legado/app/help/config/LocalConfig.kt

@@ -4,6 +4,7 @@ import android.content.Context
 import android.content.SharedPreferences
 import android.content.SharedPreferences
 import androidx.core.content.edit
 import androidx.core.content.edit
 import io.legado.app.data.local.preferences.LocalPreferencesKeys
 import io.legado.app.data.local.preferences.LocalPreferencesKeys
+import io.legado.app.constant.PreferKey
 import io.legado.app.data.repository.SettingsRepository
 import io.legado.app.data.repository.SettingsRepository
 import io.legado.app.utils.getBoolean
 import io.legado.app.utils.getBoolean
 import io.legado.app.utils.putBoolean
 import io.legado.app.utils.putBoolean
@@ -64,9 +65,9 @@ by appCtx.getSharedPreferences("local", Context.MODE_PRIVATE) {
         }
         }
 
 
     var navExtended: Boolean
     var navExtended: Boolean
-        get() = getBoolean("navExtended")
+        get() = getBoolean(PreferKey.navExtended)
         set(value) {
         set(value) {
-            putBoolean("navExtended", value)
+            putBoolean(PreferKey.navExtended, value)
         }
         }
 
 
     /** 旧版语言偏好是否已迁移到 AppCompat per-app locales(App.onCreate 一次性迁移) */
     /** 旧版语言偏好是否已迁移到 AppCompat per-app locales(App.onCreate 一次性迁移) */

+ 0 - 10
app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt

@@ -15,9 +15,6 @@ import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asStateFlow
 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.flow.update
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
@@ -41,13 +38,6 @@ class BackupConfigViewModel(
                 _uiState.update { it.copy(settings = 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) {
     fun onIntent(intent: BackupConfigIntent) {

+ 0 - 3
app/src/main/java/io/legado/app/ui/config/customTheme/CustomThemeRouteScreen.kt

@@ -5,9 +5,7 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalContext
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import io.legado.app.constant.EventBus
 import io.legado.app.lib.theme.ThemeStore
 import io.legado.app.lib.theme.ThemeStore
-import io.legado.app.utils.postEvent
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.collectLatest
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
 
 
@@ -24,7 +22,6 @@ fun CustomThemeRouteScreen(
             when (effect) {
             when (effect) {
                 is CustomThemeEffect.ApplyLegacyPrimarySeed -> {
                 is CustomThemeEffect.ApplyLegacyPrimarySeed -> {
                     ThemeStore.editTheme(context).primaryColor(effect.color).apply()
                     ThemeStore.editTheme(context).primaryColor(effect.color).apply()
-                    postEvent(EventBus.RECREATE, "")
                 }
                 }
                 is CustomThemeEffect.SettingsUpdateFailed -> {
                 is CustomThemeEffect.SettingsUpdateFailed -> {
                     Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
                     Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()

+ 1 - 1
app/src/main/java/io/legado/app/ui/config/mainConfig/MainConfig.kt

@@ -21,7 +21,7 @@ object MainConfig {
     var tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto")
     var tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto")
     var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto")
     var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto")
     var swipeAnimation by prefDelegate(PreferKey.swipeAnimation, true)
     var swipeAnimation by prefDelegate(PreferKey.swipeAnimation, true)
-    var navExtended by prefDelegate("navExtended", false)
+    var navExtended by prefDelegate(PreferKey.navExtended, false)
     var webServiceAutoStart by prefDelegate(PreferKey.webServiceAutoStart, false)
     var webServiceAutoStart by prefDelegate(PreferKey.webServiceAutoStart, false)
     var autoRefreshBook by prefDelegate(PreferKey.autoRefresh, false)
     var autoRefreshBook by prefDelegate(PreferKey.autoRefresh, false)
     var autoCheckNewBackup by prefDelegate(PreferKey.autoCheckNewBackup, true)
     var autoCheckNewBackup by prefDelegate(PreferKey.autoCheckNewBackup, true)

+ 11 - 10
app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt

@@ -37,6 +37,7 @@ import splitties.init.appCtx
 class OtherConfigViewModel(
 class OtherConfigViewModel(
     private val readAloudSettingsGateway: ReadAloudSettingsGateway,
     private val readAloudSettingsGateway: ReadAloudSettingsGateway,
     private val otherSettingsGateway: OtherSettingsGateway,
     private val otherSettingsGateway: OtherSettingsGateway,
+    initialState: OtherConfigUiState = defaultOtherConfigUiState(),
 ) : ViewModel() {
 ) : ViewModel() {
 
 
     companion object {
     companion object {
@@ -52,16 +53,7 @@ class OtherConfigViewModel(
     private var clearWebViewDataJob: Job? = null
     private var clearWebViewDataJob: Job? = null
     private var restartScheduled = false
     private var restartScheduled = false
 
 
-    private val _uiState = MutableStateFlow(
-        OtherConfigUiState(
-            checkSourceTimeoutSeconds = CheckSource.timeout / 1000,
-            checkSearch = CheckSource.checkSearch,
-            checkDiscovery = CheckSource.checkDiscovery,
-            checkInfo = CheckSource.checkInfo,
-            checkCategory = CheckSource.checkCategory,
-            checkContent = CheckSource.checkContent,
-        )
-    )
+    private val _uiState = MutableStateFlow(initialState)
     val uiState = _uiState.asStateFlow()
     val uiState = _uiState.asStateFlow()
 
 
     private val _effects = MutableSharedFlow<OtherConfigEffect>(extraBufferCapacity = 16)
     private val _effects = MutableSharedFlow<OtherConfigEffect>(extraBufferCapacity = 16)
@@ -386,6 +378,15 @@ class OtherConfigViewModel(
 
 
 }
 }
 
 
+internal fun defaultOtherConfigUiState() = OtherConfigUiState(
+    checkSourceTimeoutSeconds = CheckSource.timeout / 1000,
+    checkSearch = CheckSource.checkSearch,
+    checkDiscovery = CheckSource.checkDiscovery,
+    checkInfo = CheckSource.checkInfo,
+    checkCategory = CheckSource.checkCategory,
+    checkContent = CheckSource.checkContent,
+)
+
 private fun OtherSettings.toUiState(current: OtherConfigUiState): OtherConfigUiState =
 private fun OtherSettings.toUiState(current: OtherConfigUiState): OtherConfigUiState =
     current.copy(
     current.copy(
         language = language,
         language = language,

+ 6 - 16
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt

@@ -83,9 +83,7 @@ object ThemeConfig {
     //m3 or miuix
     //m3 or miuix
     var composeEngine by prefDelegate(PreferKey.composeEngine, "material")
     var composeEngine by prefDelegate(PreferKey.composeEngine, "material")
 
 
-    var useMiuixMonet by prefDelegate(PreferKey.useMiuixMonet, false) {
-        postEvent(EventBus.RECREATE, "")
-    }
+    var useMiuixMonet by prefDelegate(PreferKey.useMiuixMonet, false)
 
 
     var materialVersion by prefDelegate(PreferKey.materialVersion, "material3")
     var materialVersion by prefDelegate(PreferKey.materialVersion, "material3")
 
 
@@ -124,13 +122,9 @@ object ThemeConfig {
 
 
     var customMode by prefDelegate<String?>(PreferKey.customMode, "tonalSpot")
     var customMode by prefDelegate<String?>(PreferKey.customMode, "tonalSpot")
 
 
-    var fontScale by prefDelegate(PreferKey.fontScale, 10) {
-        postEvent(EventBus.RECREATE, "")
-    }
+    var fontScale by prefDelegate(PreferKey.fontScale, 10)
 
 
-    var appFontPath by prefDelegate<String?>(PreferKey.appFontPath, null) {
-        postEvent(EventBus.RECREATE, "")
-    }
+    var appFontPath by prefDelegate<String?>(PreferKey.appFontPath, null)
 
 
     var cPrimary by prefDelegate(PreferKey.cPrimary, 0)
     var cPrimary by prefDelegate(PreferKey.cPrimary, 0)
 
 
@@ -193,13 +187,9 @@ object ThemeConfig {
 
 
     var bookInfoInputColor by prefDelegate(PreferKey.bookInfoInputColor, 0)
     var bookInfoInputColor by prefDelegate(PreferKey.bookInfoInputColor, 0)
 
 
-    var cNPrimary by prefDelegate(PreferKey.cNPrimary, 0) {
-        postEvent(EventBus.RECREATE, "")
-    }
+    var cNPrimary by prefDelegate(PreferKey.cNPrimary, 0)
 
 
-    var customContrast by prefDelegate(PreferKey.customContrast, "Default") {
-        postEvent(EventBus.RECREATE, "")
-    }
+    var customContrast by prefDelegate(PreferKey.customContrast, "Default")
 
 
     var launcherIcon by prefDelegate(PreferKey.launcherIcon, "ic_launcher")
     var launcherIcon by prefDelegate(PreferKey.launcherIcon, "ic_launcher")
 
 
@@ -251,7 +241,7 @@ object ThemeConfig {
         "home,bookshelf,explore,rss,my",
         "home,bookshelf,explore,rss,my",
     )
     )
 
 
-    var navExtended by prefDelegate("navExtended", false)
+    var navExtended by prefDelegate(PreferKey.navExtended, false)
 
 
     var webServiceAutoStart by prefDelegate(PreferKey.webServiceAutoStart, false)
     var webServiceAutoStart by prefDelegate(PreferKey.webServiceAutoStart, false)
 
 

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

@@ -65,7 +65,6 @@ sealed interface ThemeConfigIntent {
 
 
 sealed interface ThemeConfigEffect {
 sealed interface ThemeConfigEffect {
     data object ApplyDayNight : ThemeConfigEffect
     data object ApplyDayNight : ThemeConfigEffect
-    data object Recreate : ThemeConfigEffect
     data object NotifyMain : ThemeConfigEffect
     data object NotifyMain : ThemeConfigEffect
     data class ChangeLauncherIcon(val value: String) : ThemeConfigEffect
     data class ChangeLauncherIcon(val value: String) : ThemeConfigEffect
     data object OpenFontFolder : ThemeConfigEffect
     data object OpenFontFolder : ThemeConfigEffect

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

@@ -77,7 +77,6 @@ fun ThemeConfigRouteScreen(
         viewModel.effects.collectLatest { effect ->
         viewModel.effects.collectLatest { effect ->
             when (effect) {
             when (effect) {
                 ThemeConfigEffect.ApplyDayNight -> ThemeConfigStore.applyDayNightLive()
                 ThemeConfigEffect.ApplyDayNight -> ThemeConfigStore.applyDayNightLive()
-                ThemeConfigEffect.Recreate -> postEvent(EventBus.RECREATE, "")
                 ThemeConfigEffect.NotifyMain -> postEvent(EventBus.NOTIFY_MAIN, true)
                 ThemeConfigEffect.NotifyMain -> postEvent(EventBus.NOTIFY_MAIN, true)
                 is ThemeConfigEffect.ChangeLauncherIcon ->
                 is ThemeConfigEffect.ChangeLauncherIcon ->
                     LauncherIconHelp.changeIcon(effect.value)
                     LauncherIconHelp.changeIcon(effect.value)

+ 0 - 2
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigViewModel.kt

@@ -162,7 +162,6 @@ class ThemeConfigViewModel(
                 add(ThemeSettingsUpdate.AppTheme(value))
                 add(ThemeSettingsUpdate.AppTheme(value))
             }
             }
             themeSettingsGateway.updateAll(updates)
             themeSettingsGateway.updateAll(updates)
-            _effects.tryEmit(ThemeConfigEffect.Recreate)
         }
         }
     }
     }
 
 
@@ -182,7 +181,6 @@ class ThemeConfigViewModel(
                     }
                     }
                 }
                 }
             )
             )
-            _effects.tryEmit(ThemeConfigEffect.Recreate)
         }
         }
     }
     }
 
 

+ 15 - 0
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt

@@ -240,6 +240,21 @@ fun BookshelfScreen(
         }
         }
     }
     }
 
 
+    LaunchedEffect(uiState.pendingUploadUrl) {
+        val url = uiState.pendingUploadUrl ?: return@LaunchedEffect
+        val result = snackbarHostState.showSnackbar(
+            message = "上传成功: $url",
+            actionLabel = "复制链接",
+            withDismissAction = true,
+        )
+        if (result == SnackbarResult.ActionPerformed) {
+            clipboardManager.setClipEntry(
+                ClipEntry(ClipData.newPlainText("url", url))
+            )
+        }
+        onIntent(BookshelfIntent.UploadResultConsumed)
+    }
+
     val importLauncher = rememberLauncherForActivityResult(
     val importLauncher = rememberLauncherForActivityResult(
         contract = ActivityResultContracts.OpenDocument(),
         contract = ActivityResultContracts.OpenDocument(),
         onResult = { uri ->
         onResult = { uri ->

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

@@ -65,6 +65,7 @@ sealed interface BookshelfIntent {
     data class UpdateSetting(val update: BookshelfSettingsUpdate) : BookshelfIntent
     data class UpdateSetting(val update: BookshelfSettingsUpdate) : BookshelfIntent
     data class UpdateThemeSetting(val update: ThemeSettingsUpdate) : BookshelfIntent
     data class UpdateThemeSetting(val update: ThemeSettingsUpdate) : BookshelfIntent
     data class SetCustomTagColors(val colors: List<TagColorPair>) : BookshelfIntent
     data class SetCustomTagColors(val colors: List<TagColorPair>) : BookshelfIntent
+    data object UploadResultConsumed : BookshelfIntent
 }
 }
 
 
 sealed interface BookshelfEffect {
 sealed interface BookshelfEffect {
@@ -113,4 +114,5 @@ data class BookshelfUiState(
     val enableCustomTagColors: Boolean = false,
     val enableCustomTagColors: Boolean = false,
     val customTagColors: ImmutableList<TagColorPair> = persistentListOf(),
     val customTagColors: ImmutableList<TagColorPair> = persistentListOf(),
     val themeColor: Int = 0,
     val themeColor: Int = 0,
+    val pendingUploadUrl: String? = null,
 ) : ListUiState<BookUiItem>
 ) : ListUiState<BookUiItem>

+ 19 - 16
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt

@@ -8,7 +8,6 @@ import io.legado.app.base.BaseViewModel
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.EventBus
-import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSource
@@ -111,6 +110,7 @@ class BookshelfViewModel(
     private val draggingBooksFlow = MutableStateFlow<List<BookUiItem>?>(null)
     private val draggingBooksFlow = MutableStateFlow<List<BookUiItem>?>(null)
     private val pendingSavedBooksFlow = MutableStateFlow<List<BookUiItem>?>(null)
     private val pendingSavedBooksFlow = MutableStateFlow<List<BookUiItem>?>(null)
     private val isInitialLoadingFlow = MutableStateFlow(true)
     private val isInitialLoadingFlow = MutableStateFlow(true)
+    private val pendingUploadUrlFlow = MutableStateFlow<String?>(null)
 
 
     private data class BookshelfSortConfig(
     private data class BookshelfSortConfig(
         val sort: Int,
         val sort: Int,
@@ -141,7 +141,8 @@ class BookshelfViewModel(
     private var cacheBookJob: Job? = null
     private var cacheBookJob: Job? = null
     private val eventListenerSource = ConcurrentHashMap<BookSource, Boolean>()
     private val eventListenerSource = ConcurrentHashMap<BookSource, Boolean>()
 
 
-    val scrollTrigger = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+    private val _scrollTrigger = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
+    val scrollTrigger = _scrollTrigger.asSharedFlow()
 
 
     private val updateConcurrency: Int
     private val updateConcurrency: Int
         get() = AppConfig.threadCount.coerceIn(1, AppConst.MAX_THREAD)
         get() = AppConfig.threadCount.coerceIn(1, AppConst.MAX_THREAD)
@@ -534,13 +535,15 @@ class BookshelfViewModel(
         bookshelfSettings,
         bookshelfSettings,
         appShellSettingsGateway.settings,
         appShellSettingsGateway.settings,
         themeSettingsGateway.settings,
         themeSettingsGateway.settings,
-    ) { state, settings, appShellSettings, themeSettings ->
+        pendingUploadUrlFlow,
+    ) { state, settings, appShellSettings, themeSettings, pendingUploadUrl ->
         state.copy(
         state.copy(
             settings = settings,
             settings = settings,
             useRaisedBottomInset = appShellSettings.useFloatingBottomBar || themeSettings.enableBlur,
             useRaisedBottomInset = appShellSettings.useFloatingBottomBar || themeSettings.enableBlur,
             enableCustomTagColors = themeSettings.enableCustomTagColors,
             enableCustomTagColors = themeSettings.enableCustomTagColors,
             customTagColors = parseTagColors(themeSettings.customTagColorsJson),
             customTagColors = parseTagColors(themeSettings.customTagColorsJson),
             themeColor = themeSettings.themeColor,
             themeColor = themeSettings.themeColor,
+            pendingUploadUrl = pendingUploadUrl,
         )
         )
     }.stateIn(
     }.stateIn(
         viewModelScope,
         viewModelScope,
@@ -573,8 +576,13 @@ class BookshelfViewModel(
         }
         }
         viewModelScope.launch {
         viewModelScope.launch {
             bookshelfSettings.collect { settings ->
             bookshelfSettings.collect { settings ->
+                if (groupIdFlow.value != settings.saveTabPosition) {
+                    groupIdFlow.value = settings.saveTabPosition
+                    clearSelection()
+                    clearDragState()
+                }
                 updateBookGroupStyle(settings.bookGroupStyle)
                 updateBookGroupStyle(settings.bookGroupStyle)
-                    postUpBooksCount()
+                postUpBooksCount()
             }
             }
         }
         }
 
 
@@ -639,6 +647,7 @@ class BookshelfViewModel(
                     )
                     )
                 )
                 )
             }
             }
+            BookshelfIntent.UploadResultConsumed -> pendingUploadUrlFlow.value = null
         }
         }
     }
     }
 
 
@@ -788,12 +797,12 @@ class BookshelfViewModel(
         val maxOrder = reorderedBooks.size
         val maxOrder = reorderedBooks.size
         execute {
         execute {
             val updates = reorderedBooks.mapIndexedNotNull { index, bookUi ->
             val updates = reorderedBooks.mapIndexedNotNull { index, bookUi ->
-                appDb.bookDao.getBook(bookUi.book.bookUrl)?.apply {
+                bookRepository.getBook(bookUi.book.bookUrl)?.apply {
                     order = if (isDescending) maxOrder - index else index + 1
                     order = if (isDescending) maxOrder - index else index + 1
                 }
                 }
             }
             }
             if (updates.isNotEmpty()) {
             if (updates.isNotEmpty()) {
-                appDb.bookDao.update(*updates.toTypedArray())
+                bookRepository.update(*updates.toTypedArray())
             }
             }
         }.onError {
         }.onError {
             showMessage("排序保存失败\n${it.localizedMessage}")
             showMessage("排序保存失败\n${it.localizedMessage}")
@@ -864,11 +873,11 @@ class BookshelfViewModel(
     }
     }
 
 
     fun gotoTop() {
     fun gotoTop() {
-        scrollTrigger.tryEmit(Unit)
+        _scrollTrigger.tryEmit(Unit)
     }
     }
     fun upAllBookToc() {
     fun upAllBookToc() {
         execute {
         execute {
-            addToWaitUp(appDb.bookDao.hasUpdateBooks)
+            addToWaitUp(bookRepository.getHasUpdateBooks())
         }
         }
     }
     }
 
 
@@ -884,7 +893,7 @@ class BookshelfViewModel(
     ) {
     ) {
         execute(context = updateDispatcher) {
         execute(context = updateDispatcher) {
             val bookUrls = books.filter { !it.isLocal && it.canUpdate }.map { it.bookUrl }
             val bookUrls = books.filter { !it.isLocal && it.canUpdate }.map { it.bookUrl }
-            val fullBooks = bookUrls.mapNotNull { appDb.bookDao.getBook(it) }
+            val fullBooks = bookUrls.mapNotNull { bookRepository.getBook(it) }
             addToWaitUp(fullBooks)
             addToWaitUp(fullBooks)
         }.onError {
         }.onError {
             if (resetRefreshWhenIdle) {
             if (resetRefreshWhenIdle) {
@@ -1078,13 +1087,7 @@ class BookshelfViewModel(
                 contentType = "application/json"
                 contentType = "application/json"
             )
             )
         }.onSuccess { url ->
         }.onSuccess { url ->
-            _effects.tryEmit(
-                BookshelfEffect.ShowSnackbar(
-                    message = "上传成功: $url",
-                    actionLabel = "复制链接",
-                    url = url
-                )
-            )
+            pendingUploadUrlFlow.value = url
         }.onError {
         }.onError {
             _effects.tryEmit(
             _effects.tryEmit(
                 BookshelfEffect.ShowSnackbar(
                 BookshelfEffect.ShowSnackbar(

+ 16 - 3
app/src/main/java/io/legado/app/ui/theme/AppTheme.kt

@@ -9,6 +9,7 @@ import androidx.compose.runtime.CompositionLocalProvider
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.remember
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalInspectionMode
 import androidx.compose.ui.platform.LocalInspectionMode
 import top.yukonga.miuix.kmp.theme.ColorSchemeMode
 import top.yukonga.miuix.kmp.theme.ColorSchemeMode
 import com.materialkolor.PaletteStyle
 import com.materialkolor.PaletteStyle
@@ -17,6 +18,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.domain.gateway.AppShellSettingsGateway
 import io.legado.app.domain.gateway.AppShellSettingsGateway
 import io.legado.app.domain.gateway.ThemeSettingsGateway
 import io.legado.app.domain.gateway.ThemeSettingsGateway
 import io.legado.app.domain.model.settings.customColors
 import io.legado.app.domain.model.settings.customColors
+import io.legado.app.utils.sysConfiguration
+import androidx.compose.ui.unit.Density
 import org.koin.compose.koinInject
 import org.koin.compose.koinInject
 
 
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@@ -90,10 +93,18 @@ private fun AppThemeActual(
     val isPureBlack = themeSettings.isPureBlack
     val isPureBlack = themeSettings.isPureBlack
     val paletteStyleValue = themeSettings.paletteStyle
     val paletteStyleValue = themeSettings.paletteStyle
     val materialVersion = themeSettings.materialVersion
     val materialVersion = themeSettings.materialVersion
+    val customContrast = themeSettings.customContrast
     val composeEngine = appShellSettings.composeEngine
     val composeEngine = appShellSettings.composeEngine
     val customPrimary = themeSettings.customPrimary
     val customPrimary = themeSettings.customPrimary
     val customNightPrimary = themeSettings.customNightPrimary
     val customNightPrimary = themeSettings.customNightPrimary
     val appFontPath = themeSettings.appFontPath
     val appFontPath = themeSettings.appFontPath
+    val currentDensity = LocalDensity.current
+    val fontScale = (appShellSettings.fontScale / 10f)
+        .takeIf { it in 0.8f..1.6f }
+        ?: sysConfiguration.fontScale
+    val appDensity = remember(currentDensity.density, fontScale) {
+        Density(currentDensity.density, fontScale)
+    }
 
 
     // 2. 深度个性化配置
     // 2. 深度个性化配置
     val enableDeepPersonalization = themeSettings.enableDeepPersonalization
     val enableDeepPersonalization = themeSettings.enableDeepPersonalization
@@ -106,7 +117,7 @@ private fun AppThemeActual(
     val colorScheme = remember(
     val colorScheme = remember(
         context, appThemeMode, effectiveDarkTheme, isPureBlack, customPrimary, customNightPrimary,
         context, appThemeMode, effectiveDarkTheme, isPureBlack, customPrimary, customNightPrimary,
         enableDeepPersonalization, customColors,
         enableDeepPersonalization, customColors,
-        paletteStyleValue, materialVersion
+        paletteStyleValue, materialVersion, customContrast,
     ) {
     ) {
         if (appThemeMode == AppThemeMode.Custom &&
         if (appThemeMode == AppThemeMode.Custom &&
             enableDeepPersonalization &&
             enableDeepPersonalization &&
@@ -130,7 +141,8 @@ private fun AppThemeActual(
                 isAmoled = isPureBlack,
                 isAmoled = isPureBlack,
                 paletteStyle = paletteStyleValue,
                 paletteStyle = paletteStyleValue,
                 materialVersion = materialVersion,
                 materialVersion = materialVersion,
-                customSeedColor = customSeedColor
+                customSeedColor = customSeedColor,
+                customContrast = customContrast,
             )
             )
         }
         }
     }
     }
@@ -171,7 +183,8 @@ private fun AppThemeActual(
 
 
     // 7. 提供主题数据并根据引擎渲染
     // 7. 提供主题数据并根据引擎渲染
     CompositionLocalProvider(
     CompositionLocalProvider(
-        LocalLegadoThemeColors provides themeColors
+        LocalLegadoThemeColors provides themeColors,
+        LocalDensity provides appDensity,
     ) {
     ) {
         if (ThemeResolver.isMiuixEngine(themeColors.composeEngine)) {
         if (ThemeResolver.isMiuixEngine(themeColors.composeEngine)) {
             MiuixThemeWrapper(
             MiuixThemeWrapper(

+ 1 - 2
app/src/main/java/io/legado/app/ui/theme/CustomColorScheme.kt

@@ -10,10 +10,9 @@ class CustomColorScheme(
     seed: Int,
     seed: Int,
     style: PaletteStyle,
     style: PaletteStyle,
     colorSpec: ThemeColorSpec = ThemeColorSpec.SPEC_2021,
     colorSpec: ThemeColorSpec = ThemeColorSpec.SPEC_2021,
+    contrastLevel: Double = ThemeResolver.resolveContrastLevel(),
 ) : BaseColorScheme() {
 ) : BaseColorScheme() {
 
 
-    private val contrastLevel: Double = ThemeResolver.resolveContrastLevel()
-
     private val specVersion = resolveColorSpecVersion(colorSpec)
     private val specVersion = resolveColorSpecVersion(colorSpec)
 
 
     override val lightScheme: ColorScheme = dynamicColorScheme(
     override val lightScheme: ColorScheme = dynamicColorScheme(

+ 19 - 6
app/src/main/java/io/legado/app/ui/theme/ThemeEngine.kt

@@ -7,6 +7,7 @@ import androidx.compose.material3.dynamicDarkColorScheme
 import androidx.compose.material3.dynamicLightColorScheme
 import androidx.compose.material3.dynamicLightColorScheme
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
 import io.legado.app.lib.theme.primaryColor
 import io.legado.app.lib.theme.primaryColor
+import io.legado.app.ui.config.themeConfig.ThemeConfig
 import io.legado.app.ui.theme.ThemeResolver.resolvePaletteStyle
 import io.legado.app.ui.theme.ThemeResolver.resolvePaletteStyle
 import io.legado.app.ui.theme.colorScheme.AugustColorScheme
 import io.legado.app.ui.theme.colorScheme.AugustColorScheme
 import io.legado.app.ui.theme.colorScheme.CarlottaColorScheme
 import io.legado.app.ui.theme.colorScheme.CarlottaColorScheme
@@ -46,7 +47,8 @@ object ThemeEngine {
         paletteStyle: String?,
         paletteStyle: String?,
         materialVersion: String? = null,
         materialVersion: String? = null,
         forceOpaque: Boolean = false,
         forceOpaque: Boolean = false,
-        customSeedColor: Int? = null
+        customSeedColor: Int? = null,
+        customContrast: String? = null,
     ): ColorScheme {
     ): ColorScheme {
         val resolvedMode = resolveMode(mode = mode, forceOpaque = forceOpaque)
         val resolvedMode = resolveMode(mode = mode, forceOpaque = forceOpaque)
         val baseColorScheme = resolveBaseColorScheme(
         val baseColorScheme = resolveBaseColorScheme(
@@ -55,7 +57,8 @@ object ThemeEngine {
             darkTheme = darkTheme,
             darkTheme = darkTheme,
             paletteStyle = paletteStyle,
             paletteStyle = paletteStyle,
             materialVersion = materialVersion,
             materialVersion = materialVersion,
-            customSeedColor = customSeedColor
+            customSeedColor = customSeedColor,
+            customContrast = customContrast,
         )
         )
 
 
         return baseColorScheme
         return baseColorScheme
@@ -80,7 +83,8 @@ object ThemeEngine {
         darkTheme: Boolean,
         darkTheme: Boolean,
         paletteStyle: String?,
         paletteStyle: String?,
         materialVersion: String?,
         materialVersion: String?,
-        customSeedColor: Int?
+        customSeedColor: Int?,
+        customContrast: String?,
     ): ColorScheme {
     ): ColorScheme {
         if (mode == AppThemeMode.Dynamic) {
         if (mode == AppThemeMode.Dynamic) {
             return resolveDynamicColorScheme(context = context, darkTheme = darkTheme)
             return resolveDynamicColorScheme(context = context, darkTheme = darkTheme)
@@ -90,7 +94,8 @@ object ThemeEngine {
                 seedColor = customSeedColor ?: context.primaryColor,
                 seedColor = customSeedColor ?: context.primaryColor,
                 darkTheme = darkTheme,
                 darkTheme = darkTheme,
                 paletteStyle = paletteStyle,
                 paletteStyle = paletteStyle,
-                materialVersion = materialVersion
+                materialVersion = materialVersion,
+                customContrast = customContrast,
             )
             )
         }
         }
         return (predefinedColorSchemes[mode] ?: GRColorScheme).getColorScheme(darkTheme)
         return (predefinedColorSchemes[mode] ?: GRColorScheme).getColorScheme(darkTheme)
@@ -110,11 +115,19 @@ object ThemeEngine {
         seedColor: Int,
         seedColor: Int,
         darkTheme: Boolean,
         darkTheme: Boolean,
         paletteStyle: String?,
         paletteStyle: String?,
-        materialVersion: String?
+        materialVersion: String?,
+        customContrast: String?,
     ): ColorScheme {
     ): ColorScheme {
         val style = resolvePaletteStyle(paletteStyle)
         val style = resolvePaletteStyle(paletteStyle)
         val colorSpec = ThemeResolver.resolveColorSpecFromMaterialVersion(materialVersion)
         val colorSpec = ThemeResolver.resolveColorSpecFromMaterialVersion(materialVersion)
-        return CustomColorScheme(seedColor, style, colorSpec).getColorScheme(darkTheme)
+        return CustomColorScheme(
+            seed = seedColor,
+            style = style,
+            colorSpec = colorSpec,
+            contrastLevel = ThemeResolver.resolveContrastLevel(
+                customContrast ?: ThemeConfig.customContrast
+            ),
+        ).getColorScheme(darkTheme)
     }
     }
 
 
     private fun ColorScheme.applyAmoledIfNeeded(
     private fun ColorScheme.applyAmoledIfNeeded(

+ 2 - 2
app/src/main/java/io/legado/app/ui/theme/ThemeResolver.kt

@@ -69,8 +69,8 @@ object ThemeResolver {
         return materialPaletteStyles[value] ?: PaletteStyle.TonalSpot
         return materialPaletteStyles[value] ?: PaletteStyle.TonalSpot
     }
     }
 
 
-    fun resolveContrastLevel(): Double {
-        return runCatching { Contrast.valueOf(ThemeConfig.customContrast).value }
+    fun resolveContrastLevel(value: String = ThemeConfig.customContrast): Double {
+        return runCatching { Contrast.valueOf(value).value }
             .getOrDefault(Contrast.Default.value)
             .getOrDefault(Contrast.Default.value)
     }
     }
 
 

+ 82 - 0
app/src/test/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModelTest.kt

@@ -0,0 +1,82 @@
+package io.legado.app.ui.config.otherConfig
+
+import android.app.Application
+import android.os.Looper
+import io.legado.app.domain.gateway.OtherSettingsGateway
+import io.legado.app.domain.gateway.OtherSettingsUpdate
+import io.legado.app.domain.gateway.ReadAloudSettingsGateway
+import io.legado.app.domain.gateway.ReadAloudSettingsUpdate
+import io.legado.app.domain.model.settings.OtherSettings
+import io.legado.app.domain.model.settings.ReadAloudSettings
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.async
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.annotation.Config
+import splitties.init.injectAsAppCtx
+
+@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class, sdk = [35])
+class OtherConfigViewModelTest {
+
+    @Before
+    fun setUp() {
+        RuntimeEnvironment.getApplication().injectAsAppCtx()
+    }
+
+    @Test
+    fun languageChanged_updatesGatewayAndEmitsApplyLanguage() = runBlocking {
+        val otherSettingsGateway = FakeOtherSettingsGateway()
+        val viewModel = OtherConfigViewModel(
+            readAloudSettingsGateway = FakeReadAloudSettingsGateway(),
+            otherSettingsGateway = otherSettingsGateway,
+            initialState = OtherConfigUiState(),
+        )
+        val effect = async(start = CoroutineStart.UNDISPATCHED) {
+            viewModel.effects.first()
+        }
+
+        viewModel.onIntent(OtherConfigIntent.LanguageChanged("en"))
+        Shadows.shadowOf(Looper.getMainLooper()).idle()
+
+        assertEquals(OtherConfigEffect.ApplyLanguage("en"), effect.await())
+        assertTrue(
+            otherSettingsGateway.updates.contains(OtherSettingsUpdate.Language("en"))
+        )
+        assertEquals("en", viewModel.uiState.value.language)
+    }
+
+    private class FakeOtherSettingsGateway : OtherSettingsGateway {
+        private val state = MutableStateFlow(OtherSettings())
+        val updates = mutableListOf<OtherSettingsUpdate>()
+
+        override val settings: Flow<OtherSettings> = state
+
+        override suspend fun update(update: OtherSettingsUpdate) {
+            updates += update
+            if (update is OtherSettingsUpdate.Language) {
+                state.value = state.value.copy(language = update.value)
+            }
+        }
+    }
+
+    private class FakeReadAloudSettingsGateway : ReadAloudSettingsGateway {
+        private val state = MutableStateFlow(ReadAloudSettings())
+
+        override val currentSettings: ReadAloudSettings
+            get() = state.value
+        override val settings: Flow<ReadAloudSettings> = state
+
+        override suspend fun update(update: ReadAloudSettingsUpdate) = Unit
+    }
+}