diff --git a/app/src/main/java/io/legado/app/help/config/AppConfig.kt b/app/src/main/java/io/legado/app/help/config/AppConfig.kt index f28dd06d8..55451fd48 100644 --- a/app/src/main/java/io/legado/app/help/config/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/AppConfig.kt @@ -1007,7 +1007,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { var pureBlack get() = appCtx.getPrefBoolean(PreferKey.pureBlack, false) set(value) { - appCtx.getPrefBoolean(PreferKey.pureBlack, value) + appCtx.putPrefBoolean(PreferKey.pureBlack, value) } val hasLightBg: Boolean diff --git a/app/src/main/java/io/legado/app/lib/prefs/ThemeCardPreference.kt b/app/src/main/java/io/legado/app/lib/prefs/ThemeCardPreference.kt index ee2a0becc..5c51318ec 100644 --- a/app/src/main/java/io/legado/app/lib/prefs/ThemeCardPreference.kt +++ b/app/src/main/java/io/legado/app/lib/prefs/ThemeCardPreference.kt @@ -19,7 +19,6 @@ import com.google.android.material.card.MaterialCardView import io.legado.app.R import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey -import io.legado.app.help.config.AppConfig import io.legado.app.lib.dialogs.alert import io.legado.app.ui.config.themeConfig.ThemeConfig import io.legado.app.utils.getPrefString @@ -90,7 +89,7 @@ class ThemeCardPreference(context: Context, attrs: AttributeSet) : Preference(co context.toastOnUi(R.string.transparent_theme_alarm) return@setOnClickListener } else { - AppConfig.containerOpacity = 0 + ThemeConfig.containerOpacity = 0 } } val oldValue = currentValue diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt index 383cc3b6a..74e4038cb 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt @@ -2587,7 +2587,6 @@ class ReadBookViewModel( else -> "0" // dark → follow system } ThemeConfig.themeMode = nextMode - AppConfig.themeMode = nextMode _uiState.update { it.copy(styleConfig = buildStyleConfig()) } _effects.tryEmit(ReadBookEffect.UpdateReadViewConfig( setOf( diff --git a/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt b/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt index 4db827c7f..6827f502a 100644 --- a/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/config/PrefDelegate.kt @@ -1,17 +1,10 @@ package io.legado.app.ui.config -import android.content.SharedPreferences import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner -import io.legado.app.utils.defaultSharedPreferences -import io.legado.app.utils.getPrefBoolean -import io.legado.app.utils.getPrefFloat -import io.legado.app.utils.getPrefInt -import io.legado.app.utils.getPrefLong -import io.legado.app.utils.getPrefString import io.legado.app.utils.putPrefBoolean import io.legado.app.utils.putPrefFloat import io.legado.app.utils.putPrefInt @@ -24,8 +17,10 @@ import io.legado.app.data.repository.dataStore import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import splitties.init.appCtx @@ -49,39 +44,41 @@ fun prefDelegate( sync: Boolean = false, onValueChange: ((T) -> Unit)? = null ): PrefDelegate { - return object : PrefDelegate, SharedPreferences.OnSharedPreferenceChangeListener, DefaultLifecycleObserver { - private var _value: MutableState = mutableStateOf(readInitialValue()) + return object : PrefDelegate, DefaultLifecycleObserver { + private var _value: MutableState = mutableStateOf(defaultValue) override val state: State get() = _value + private val scope = CoroutineScope(Dispatchers.IO) private var dsObserverJob: Job? = null init { - if (lifecycleOwner != null) { - lifecycleOwner.lifecycle.addObserver(this) - } else { - appCtx.defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this) + // 从 DataStore 读取初始值(DS 为唯一读取源) + scope.launch { + val dsValue = readFromDs() + if (dsValue != null) { + _value.value = dsValue + } } - // 观察 DataStore,以 DS 为读取权威源 - dsObserverJob = CoroutineScope(Dispatchers.IO).launch { + // 观察 DataStore 变化,用于跨实例同步 + dsObserverJob = scope.launch { appCtx.dataStore.data .catch { if (it is IOException) emit(emptyPreferences()) else throw it } .map { prefs -> - // 安全读取 String 值,用于类型回退(类型不匹配时返回 null) val strVal = runCatching { prefs[stringPreferencesKey(key)] }.getOrNull() @Suppress("UNCHECKED_CAST") when { defaultValue is String || defaultValue == null -> strVal as T? defaultValue is Int -> - (prefs[intPreferencesKey(key)] + (runCatching { prefs[intPreferencesKey(key)] }.getOrNull() ?: strVal?.toIntOrNull()) as T? defaultValue is Boolean -> - (prefs[booleanPreferencesKey(key)] + (runCatching { prefs[booleanPreferencesKey(key)] }.getOrNull() ?: strVal?.toBooleanStrictOrNull()) as T? defaultValue is Long -> - (prefs[longPreferencesKey(key)] + (runCatching { prefs[longPreferencesKey(key)] }.getOrNull() ?: strVal?.toLongOrNull()) as T? defaultValue is Float -> - (prefs[floatPreferencesKey(key)] + (runCatching { prefs[floatPreferencesKey(key)] }.getOrNull() ?: strVal?.toFloatOrNull()) as T? else -> null } @@ -94,10 +91,10 @@ fun prefDelegate( } } } - } - - override fun onCreate(owner: LifecycleOwner) { - appCtx.defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this) + // 注册生命周期观察者(如果有) + if (lifecycleOwner != null) { + lifecycleOwner.lifecycle.addObserver(this) + } } override fun onDestroy(owner: LifecycleOwner) { @@ -105,33 +102,17 @@ fun prefDelegate( } override fun dispose() { - appCtx.defaultSharedPreferences.unregisterOnSharedPreferenceChangeListener(this) dsObserverJob?.cancel() } - @Suppress("UNCHECKED_CAST") - private fun readInitialValue(): T { - val prefs = appCtx.defaultSharedPreferences - return when { - defaultValue is String -> appCtx.getPrefString(key, defaultValue) as T - defaultValue == null && prefs.contains(key) -> { - appCtx.getPrefString(key, null) as T - } - - defaultValue is Int -> appCtx.getPrefInt(key, defaultValue) as T - defaultValue is Boolean -> appCtx.getPrefBoolean(key, defaultValue) as T - defaultValue is Long -> appCtx.getPrefLong(key, defaultValue) as T - defaultValue is Float -> appCtx.getPrefFloat(key, defaultValue) as T - else -> defaultValue - } - } - override fun getValue(thisRef: Any?, property: KProperty<*>): T { return _value.value } override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { if (_value.value != value) { + _value.value = value + // 同步写入 SP(向后兼容:AppConfig/MainViewModel 等仍直接读 SP) when (value) { is String? -> if (sync) appCtx.putPrefStringSync(key, value) else appCtx.putPrefString(key, value) is Int -> appCtx.putPrefInt(key, value) @@ -139,8 +120,8 @@ fun prefDelegate( is Long -> appCtx.putPrefLong(key, value) is Float -> appCtx.putPrefFloat(key, value) } - // 同步写入 DataStore,保持 SP/DS 一致 - CoroutineScope(Dispatchers.IO).launch { + // 同步写入 DataStore(权威存储),确保持久化后再返回 + runBlocking(Dispatchers.IO) { when (value) { is String? -> DsSync.putString(key, value) is Int -> DsSync.putInt(key, value) @@ -149,18 +130,42 @@ fun prefDelegate( is Float -> DsSync.putFloat(key, value) } } - _value.value = value onValueChange?.invoke(value) } } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, changedKey: String?) { - if (changedKey == key) { - val newValue = readInitialValue() - if (_value.value != newValue) { - _value.value = newValue - onValueChange?.invoke(newValue) - } + /** + * 从 DataStore 读取当前值。 + * DS 为唯一读取源,类型不匹配时安全回退到 String 解析。 + */ + @Suppress("UNCHECKED_CAST") + private suspend fun readFromDs(): T? { + return try { + appCtx.dataStore.data + .catch { if (it is IOException) emit(emptyPreferences()) else throw it } + .map { prefs -> + val strVal = runCatching { prefs[stringPreferencesKey(key)] }.getOrNull() + when { + defaultValue is String || defaultValue == null -> + strVal as T? + defaultValue is Int -> + (runCatching { prefs[intPreferencesKey(key)] }.getOrNull() + ?: strVal?.toIntOrNull()) as T? + defaultValue is Boolean -> + (runCatching { prefs[booleanPreferencesKey(key)] }.getOrNull() + ?: strVal?.toBooleanStrictOrNull()) as T? + defaultValue is Long -> + (runCatching { prefs[longPreferencesKey(key)] }.getOrNull() + ?: strVal?.toLongOrNull()) as T? + defaultValue is Float -> + (runCatching { prefs[floatPreferencesKey(key)] }.getOrNull() + ?: strVal?.toFloatOrNull()) as T? + else -> null + } + } + .first() + } catch (e: Exception) { + null } } } @@ -175,4 +180,4 @@ fun prefStateDelegate( ): PrefStateDelegate { val delegate = prefDelegate(key, defaultValue, lifecycleOwner, sync, onValueChange) return PrefStateDelegate(delegate) -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt index c5bfd718d..a2f199f59 100644 --- a/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt +++ b/app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfigScreen.kt @@ -79,7 +79,6 @@ import io.legado.app.base.AppContextWrapper import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey import io.legado.app.help.LauncherIconHelp -import io.legado.app.help.config.AppConfig import io.legado.app.help.config.ThemeConfigStore import io.legado.app.help.loadFontFiles import io.legado.app.ui.config.labConfig.LabConfig @@ -311,7 +310,7 @@ fun ThemeConfigScreen( context.toastOnUi(R.string.transparent_theme_alarm) return@ThemeColorSelector } else { - AppConfig.containerOpacity = 0 + ThemeConfig.containerOpacity = 0 } } val oldTheme = selectedTheme diff --git a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt index 1855a0a5d..85b0ebc16 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt @@ -1,19 +1,16 @@ package io.legado.app.ui.main import android.app.Application -import android.content.SharedPreferences import androidx.compose.runtime.Stable +import androidx.compose.runtime.snapshotFlow +import androidx.lifecycle.viewModelScope import io.legado.app.base.BaseViewModel import io.legado.app.constant.EventBus -import io.legado.app.constant.PreferKey import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase import io.legado.app.domain.usecase.WebDavBackupUseCase import io.legado.app.ui.config.themeConfig.ThemeConfig import io.legado.app.ui.main.my.PrefClickEvent -import io.legado.app.utils.defaultSharedPreferences import io.legado.app.utils.eventBus.FlowEventBus -import io.legado.app.utils.getPrefBoolean -import io.legado.app.utils.getPrefString import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableSharedFlow @@ -21,6 +18,7 @@ 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 MainViewModel( application: Application, @@ -28,40 +26,25 @@ class MainViewModel( private val webDavBackupUseCase: WebDavBackupUseCase ) : BaseViewModel(application) { - private val prefs = context.defaultSharedPreferences - private val mainPreferenceKeys = setOf( - PreferKey.showDiscovery, - PreferKey.showHome, - PreferKey.showRss, - PreferKey.showBottomView, - PreferKey.useFloatingBottomBar, - PreferKey.useFloatingBottomBarLiquidGlass, - PreferKey.defaultHomePage, - PreferKey.labelVisibilityMode, - NAV_EXTENDED_KEY - ) - private val preferenceListener = - SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - if (key in mainPreferenceKeys) { - _uiState.value = readMainUiState() - } - } - private val _uiState = MutableStateFlow(readMainUiState()) val uiState = _uiState.asStateFlow() private val _effects = MutableSharedFlow(extraBufferCapacity = 8) val effects = _effects.asSharedFlow() init { - prefs.registerOnSharedPreferenceChangeListener(preferenceListener) + // 通过 snapshotFlow 直接观察 ThemeConfig 的 Compose State,全链路走 DS + viewModelScope.launch { + snapshotFlow { + readMainUiState() + }.collect { newState -> + if (_uiState.value != newState) { + _uiState.value = newState + } + } + } deleteNotShelfBook() } - override fun onCleared() { - prefs.unregisterOnSharedPreferenceChangeListener(preferenceListener) - super.onCleared() - } - fun upAllBookToc() { FlowEventBus.post(EventBus.UP_ALL_BOOK_TOC, Unit) } @@ -149,31 +132,22 @@ data class MainUiState( val navExtended: Boolean = false ) -private const val NAV_EXTENDED_KEY = "navExtended" - private fun MainViewModel.readMainUiState(): MainUiState { - val showDiscovery = context.getPrefBoolean(PreferKey.showDiscovery, true) - val showHome = context.getPrefBoolean(PreferKey.showHome, true) - val showRss = context.getPrefBoolean(PreferKey.showRss, true) val destinations = MainDestination.mainDestinations.filter { when (it) { - MainDestination.Explore -> showDiscovery - MainDestination.Home -> showHome - MainDestination.Rss -> showRss + MainDestination.Explore -> ThemeConfig.showDiscovery + MainDestination.Home -> ThemeConfig.showHome + MainDestination.Rss -> ThemeConfig.showRss else -> true } }.toImmutableList() return MainUiState( destinations = destinations, - defaultHomePage = context.getPrefString(PreferKey.defaultHomePage, "bookshelf") - ?: "bookshelf", - showBottomView = context.getPrefBoolean(PreferKey.showBottomView, true), - useFloatingBottomBar = context.getPrefBoolean(PreferKey.useFloatingBottomBar, false), - useFloatingBottomBarLiquidGlass = context.getPrefBoolean( - PreferKey.useFloatingBottomBarLiquidGlass, - false - ), - labelVisibilityMode = context.getPrefString(PreferKey.labelVisibilityMode, "auto") ?: "auto", - navExtended = context.getPrefBoolean(NAV_EXTENDED_KEY, false) + defaultHomePage = ThemeConfig.defaultHomePage, + showBottomView = ThemeConfig.showBottomView, + useFloatingBottomBar = ThemeConfig.useFloatingBottomBar, + useFloatingBottomBarLiquidGlass = ThemeConfig.useFloatingBottomBarLiquidGlass, + labelVisibilityMode = ThemeConfig.labelVisibilityMode, + navExtended = ThemeConfig.navExtended ) } diff --git a/app/src/main/java/io/legado/app/utils/ContextExtensions.kt b/app/src/main/java/io/legado/app/utils/ContextExtensions.kt index 8b58509af..161c7fe8d 100644 --- a/app/src/main/java/io/legado/app/utils/ContextExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/ContextExtensions.kt @@ -36,7 +36,6 @@ import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.content.edit import androidx.core.net.toUri -import androidx.datastore.preferences.preferencesDataStore import androidx.preference.PreferenceManager import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import io.legado.app.R @@ -464,8 +463,6 @@ val Context.channel: String val Context.isDebuggable: Boolean get() = applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0 -val Context.dataStore by preferencesDataStore(name = "settings") - val Context.bookshelfLayoutMode: Int get() = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) { AppConfig.bookshelfLayoutModeLandscape diff --git a/app/src/main/java/io/legado/app/utils/DataStoreUtil.kt b/app/src/main/java/io/legado/app/utils/DataStoreUtil.kt deleted file mode 100644 index 2d1af6311..000000000 --- a/app/src/main/java/io/legado/app/utils/DataStoreUtil.kt +++ /dev/null @@ -1,40 +0,0 @@ -package io.legado.app.utils - -import android.content.Context -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.floatPreferencesKey -import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.longPreferencesKey -import androidx.datastore.preferences.core.stringPreferencesKey -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map - -object DataStoreUtil { - - suspend fun save(context: Context, key: String, value: T) { - context.dataStore.edit { preferences -> - when (value) { - is String -> preferences[stringPreferencesKey(key)] = value - is Int -> preferences[intPreferencesKey(key)] = value - is Boolean -> preferences[booleanPreferencesKey(key)] = value - is Float -> preferences[floatPreferencesKey(key)] = value - is Long -> preferences[longPreferencesKey(key)] = value - } - } - } - - @Suppress("UNCHECKED_CAST") - suspend fun read(context: Context, key: String, defaultValue: T): T { - return context.dataStore.data.map { preferences -> - when (defaultValue) { - is String -> preferences[stringPreferencesKey(key)] ?: defaultValue - is Int -> preferences[intPreferencesKey(key)] ?: defaultValue - is Boolean -> preferences[booleanPreferencesKey(key)] ?: defaultValue - is Float -> preferences[floatPreferencesKey(key)] ?: defaultValue - is Long -> preferences[longPreferencesKey(key)] ?: defaultValue - else -> defaultValue - } as T - }.first() - } -} \ No newline at end of file