فهرست منبع

feat: 引入 AppSettings 数据类,整合应用设置,优化主题和朗读速度管理

5151561 3 ماه پیش
والد
کامیت
5ab74334ef

+ 15 - 0
app/src/main/java/io/legado/app/data/local/preferences/AppSettings.kt

@@ -0,0 +1,15 @@
+package io.legado.app.data.local.preferences
+
+import androidx.compose.runtime.Stable
+
+/**
+ * 全应用设置的“唯一真理模型” - 强类型、不可变、业务语义化
+ */
+@Stable
+data class AppSettings(
+    val darkTheme: Boolean = false,
+    val language: String = "auto",
+    val readAloudSpeed: Int = 5,
+    val textWeight: Int = 1,
+    val showBrightnessView: Boolean = true
+)

+ 65 - 0
app/src/main/java/io/legado/app/data/local/preferences/LocalPreferencesRepository.kt

@@ -1,17 +1,82 @@
 package io.legado.app.data.local.preferences
 
 import android.content.Context
+import android.content.res.Configuration
 import androidx.datastore.preferences.core.Preferences
 import androidx.datastore.preferences.core.edit
 import androidx.datastore.preferences.core.emptyPreferences
+import io.legado.app.constant.PreferKey
+import io.legado.app.help.config.AppConfigStore
+import io.legado.app.help.config.compatDsInt
+import io.legado.app.help.config.compatDsString
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
 import java.io.IOException
 
 class LocalPreferencesRepository(private val context: Context) {
 
     private val dataStore = context.localDataStore
+    private val repositoryScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+
+    // 唯一状态真理守护者:桥接自 AppConfigStore 并洗白历史脏类型
+    val appSettings: StateFlow<AppSettings> = AppConfigStore.preferencesFlow
+        .map { preferences ->
+            val themeMode = preferences.compatDsString(PreferKey.themeMode) ?: "0"
+            val isDark = when (themeMode) {
+                "1" -> false
+                "2" -> true
+                else -> {
+                    context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
+                }
+            }
+            AppSettings(
+                darkTheme = isDark,
+                language = preferences.compatDsString(PreferKey.language) ?: "auto",
+                readAloudSpeed = preferences.compatDsInt(PreferKey.ttsSpeechRate) ?: 5,
+                textWeight = preferences.compatDsInt("textWeight") ?: 1,
+                showBrightnessView = preferences.compatDsString(PreferKey.showBrightnessView) != "0"
+            )
+        }.stateIn(
+            scope = repositoryScope,
+            started = SharingStarted.Eagerly,
+            initialValue = AppSettings()
+        )
+
+    /**
+     * 优雅的闭包更新变换,物理落盘统一差异化批量投递
+     */
+    fun updateSettings(transform: (AppSettings) -> AppSettings) {
+        val current = appSettings.value
+        val updated = transform(current)
+
+        val changedPairs = mutableMapOf<String, Any?>()
+        if (updated.darkTheme != current.darkTheme) {
+            changedPairs[PreferKey.themeMode] = if (updated.darkTheme) "2" else "1"
+        }
+        if (updated.language != current.language) {
+            changedPairs[PreferKey.language] = updated.language
+        }
+        if (updated.readAloudSpeed != current.readAloudSpeed) {
+            changedPairs[PreferKey.ttsSpeechRate] = updated.readAloudSpeed
+        }
+        if (updated.textWeight != current.textWeight) {
+            changedPairs["textWeight"] = updated.textWeight
+        }
+        if (updated.showBrightnessView != current.showBrightnessView) {
+            changedPairs[PreferKey.showBrightnessView] = if (updated.showBrightnessView) "1" else "0"
+        }
+
+        if (changedPairs.isNotEmpty()) {
+            AppConfigStore.putAll(changedPairs)
+        }
+    }
 
     fun <T> getPreference(key: Preferences.Key<T>, defaultValue: T): Flow<T> {
         return dataStore.data

+ 8 - 3
app/src/main/java/io/legado/app/service/TTSReadAloudService.kt

@@ -8,13 +8,14 @@ import io.legado.app.R
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppPattern
+import io.legado.app.data.local.preferences.LocalPreferencesRepository
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.MediaHelp
-import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.lib.dialogs.SelectItem
 import io.legado.app.model.ReadAloud
 import io.legado.app.model.ReadBook
+import io.legado.app.ui.config.readConfig.ReadConfig
 import io.legado.app.utils.GSON
 import io.legado.app.utils.LogUtils
 import io.legado.app.utils.fromJsonObject
@@ -22,11 +23,15 @@ import io.legado.app.utils.servicePendingIntent
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.ensureActive
+import org.koin.core.component.KoinComponent
+import org.koin.core.component.inject
 
 /**
  * 本地朗读
  */
-class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener {
+class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener, KoinComponent {
+
+    private val localPreferencesRepository: LocalPreferencesRepository by inject()
 
     private var textToSpeech: TextToSpeech? = null
     private var ttsInitFinish = false
@@ -207,7 +212,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
                 initTts()
             }
         } else {
-            val speechRate = (ReadConfig.ttsSpeechRate + 5) / 10f
+            val speechRate = (localPreferencesRepository.appSettings.value.readAloudSpeed + 5) / 10f
             textToSpeech?.setSpeechRate(speechRate)
             if (reset && !pause) {
                 play()

+ 11 - 5
app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt

@@ -76,6 +76,7 @@ import io.legado.app.help.LauncherIconHelp
 import io.legado.app.help.config.ThemeConfigStore
 import io.legado.app.ui.config.labConfig.LabConfig
 import io.legado.app.ui.theme.LegadoTheme
+import io.legado.app.ui.theme.LocalAppSettings
 import io.legado.app.ui.theme.ThemeEngine
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.adaptiveContentPadding
@@ -95,6 +96,7 @@ 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.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
+import io.legado.app.utils.LogUtils
 import io.legado.app.utils.postEvent
 import io.legado.app.utils.takePersistablePermissionSafely
 import io.legado.app.utils.toastOnUi
@@ -108,6 +110,8 @@ fun ThemeConfigScreen(
     onNavigateToThemeManage: () -> Unit,
     viewModel: ThemeConfigViewModel = koinViewModel()
 ) {
+    val appSettings = LocalAppSettings.current
+    val systemInDark = isSystemInDarkTheme()
     val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
     var manageKey by remember { mutableStateOf<Boolean?>(null) }
     val context = LocalContext.current
@@ -167,11 +171,7 @@ fun ThemeConfigScreen(
                 val isMiuixEngine = remember(composeEngine) {
                     ThemeResolver.isMiuixEngine(composeEngine)
                 }
-                val isDarkTheme = when (selectedThemeMode) {
-                    "1" -> false
-                    "2" -> true
-                    else -> isSystemInDarkTheme()
-                }
+                val isDarkTheme = appSettings.darkTheme
 
                 if (!isMiuixEngine) {
                     Column(
@@ -233,8 +233,11 @@ fun ThemeConfigScreen(
                             entryValues = stringArrayResource(R.array.theme_mode_v),
                             onValueChange = { mode ->
                                 selectedThemeMode = mode
+                                val expectedDark = if (mode == "0") systemInDark else mode == "2"
+                                viewModel.updateSettings { it.copy(darkTheme = expectedDark) }
                                 ThemeConfig.themeMode = mode
                                 ThemeConfigStore.applyDayNightLive()
+                                LogUtils.d("ThemeConfigScreen", "Theme sync log: mode=$mode (resolved expectedDark=$expectedDark), ThemeConfig.themeMode=${ThemeConfig.themeMode}")
                             }
                         )
 
@@ -273,8 +276,11 @@ fun ThemeConfigScreen(
                             selectedMode = selectedThemeMode,
                             onModeSelected = { mode ->
                                 selectedThemeMode = mode
+                                val expectedDark = if (mode == "0") systemInDark else mode == "2"
+                                viewModel.updateSettings { it.copy(darkTheme = expectedDark) }
                                 ThemeConfig.themeMode = mode
                                 ThemeConfigStore.applyDayNightLive()
+                                LogUtils.d("ThemeConfigScreen", "Theme sync log: mode=$mode (resolved expectedDark=$expectedDark), ThemeConfig.themeMode=${ThemeConfig.themeMode}")
                             }
                         )
                     }

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

@@ -4,6 +4,7 @@ import android.net.Uri
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
 import io.legado.app.constant.PreferKey
+import io.legado.app.data.local.preferences.AppSettings
 import io.legado.app.data.local.preferences.LocalPreferencesKeys
 import io.legado.app.data.local.preferences.LocalPreferencesRepository
 import io.legado.app.data.repository.ReadSettingsRepository
@@ -28,6 +29,12 @@ class ThemeConfigViewModel(
     private val readSettingsRepository: ReadSettingsRepository
 ) : ViewModel() {
 
+    val appSettings = localPreferencesRepository.appSettings
+
+    fun updateSettings(transform: (AppSettings) -> AppSettings) {
+        localPreferencesRepository.updateSettings(transform)
+    }
+
     val fontFolder = readSettingsRepository.preferences
         .map { preferences ->
             val fontFolder: String? = preferences.fontFolder

+ 9 - 0
app/src/main/java/io/legado/app/ui/theme/AppTheme.kt

@@ -13,6 +13,11 @@ import androidx.compose.ui.platform.LocalInspectionMode
 import io.legado.app.ui.config.themeConfig.ThemeConfig
 import top.yukonga.miuix.kmp.theme.ColorSchemeMode
 import com.materialkolor.PaletteStyle
+import androidx.compose.runtime.getValue
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import io.legado.app.data.local.preferences.AppSettings
+import io.legado.app.data.local.preferences.LocalPreferencesRepository
+import org.koin.compose.koinInject
 
 @OptIn(ExperimentalMaterial3ExpressiveApi::class)
 @Composable
@@ -44,6 +49,7 @@ private fun AppThemePreview(
         composeEngine = "material"
     )
     CompositionLocalProvider(
+        LocalAppSettings provides AppSettings(),
         LocalLegadoThemeColors provides themeColors
     ) {
         MaterialThemeWrapper(
@@ -60,6 +66,8 @@ private fun AppThemeActual(
     darkTheme: Boolean,
     content: @Composable () -> Unit
 ) {
+    val preferencesRepository = koinInject<LocalPreferencesRepository>()
+    val settings by preferencesRepository.appSettings.collectAsStateWithLifecycle()
     val context = LocalContext.current
     
     // 1. 获取基础配置
@@ -154,6 +162,7 @@ private fun AppThemeActual(
 
     // 7. 提供主题数据并根据引擎渲染
     CompositionLocalProvider(
+        LocalAppSettings provides settings,
         LocalLegadoThemeColors provides themeColors
     ) {
         if (ThemeResolver.isMiuixEngine(themeColors.composeEngine)) {

+ 9 - 0
app/src/main/java/io/legado/app/ui/theme/LocalAppSettings.kt

@@ -0,0 +1,9 @@
+package io.legado.app.ui.theme
+
+import androidx.compose.runtime.staticCompositionLocalOf
+import io.legado.app.data.local.preferences.AppSettings
+
+/**
+ * 全局可访问的静态配置快照流
+ */
+val LocalAppSettings = staticCompositionLocalOf { AppSettings() }