|
|
@@ -4,16 +4,15 @@ import androidx.compose.runtime.MutableState
|
|
|
import androidx.compose.runtime.State
|
|
|
import androidx.compose.runtime.mutableStateOf
|
|
|
import androidx.compose.runtime.snapshots.Snapshot
|
|
|
-import androidx.datastore.preferences.core.booleanPreferencesKey
|
|
|
-import androidx.datastore.preferences.core.emptyPreferences
|
|
|
-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 androidx.datastore.preferences.core.Preferences
|
|
|
import androidx.lifecycle.DefaultLifecycleObserver
|
|
|
import androidx.lifecycle.LifecycleOwner
|
|
|
-import io.legado.app.data.repository.dataStore
|
|
|
-import io.legado.app.help.config.DsSync
|
|
|
+import io.legado.app.help.config.AppConfigStore
|
|
|
+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.compatDsLong
|
|
|
+import io.legado.app.help.config.compatDsString
|
|
|
import io.legado.app.utils.getPrefBoolean
|
|
|
import io.legado.app.utils.getPrefFloat
|
|
|
import io.legado.app.utils.getPrefInt
|
|
|
@@ -26,15 +25,12 @@ import io.legado.app.utils.putPrefLong
|
|
|
import io.legado.app.utils.putPrefString
|
|
|
import io.legado.app.utils.putPrefStringSync
|
|
|
import kotlinx.coroutines.CoroutineScope
|
|
|
+import kotlinx.coroutines.Dispatchers
|
|
|
import kotlinx.coroutines.Job
|
|
|
-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 kotlinx.coroutines.runBlocking
|
|
|
import splitties.init.appCtx
|
|
|
-import java.io.IOException
|
|
|
import kotlin.properties.ReadWriteProperty
|
|
|
import kotlin.reflect.KProperty
|
|
|
|
|
|
@@ -60,44 +56,19 @@ fun <T> prefDelegate(
|
|
|
|
|
|
@Volatile
|
|
|
private var currentValue: T = defaultValue
|
|
|
- private val scope = CoroutineScope(kotlinx.coroutines.Dispatchers.IO)
|
|
|
-
|
|
|
- // 单并发调度器保证同一 key 的 DataStore 写入按提交顺序落盘
|
|
|
- @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
|
|
|
- private val dsWriteDispatcher = kotlinx.coroutines.Dispatchers.IO.limitedParallelism(1)
|
|
|
+ private val scope = CoroutineScope(Dispatchers.IO)
|
|
|
private var dsObserverJob: Job? = null
|
|
|
|
|
|
init {
|
|
|
- // 同步从 DataStore 读取初始值,确保构造完成后即为最新值
|
|
|
- val initialValue = runBlocking { readFromDs() } ?: defaultValue
|
|
|
+ // 初值走门面:AppConfigStore 内存快照,DS 缺值时门面内回退 SP 并补写
|
|
|
+ val initialValue = readInitial()
|
|
|
_value = mutableStateOf(initialValue)
|
|
|
currentValue = initialValue
|
|
|
|
|
|
- // 观察 DataStore 变化,用于跨实例同步
|
|
|
+ // 观察快照变化,用于跨实例同步(含未落盘的 pending 写入,写后立即可见)
|
|
|
dsObserverJob = scope.launch {
|
|
|
- appCtx.dataStore.data
|
|
|
- .catch { if (it is IOException) emit(emptyPreferences()) else throw it }
|
|
|
- .map { prefs ->
|
|
|
- val strVal = runCatching { prefs[stringPreferencesKey(key)] }.getOrNull()
|
|
|
- @Suppress("UNCHECKED_CAST")
|
|
|
- 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
|
|
|
- }
|
|
|
- }
|
|
|
+ AppConfigStore.preferencesFlow
|
|
|
+ .map { prefs -> readTyped(prefs) }
|
|
|
.distinctUntilChanged()
|
|
|
.collect { dsValue ->
|
|
|
if (dsValue != null && currentValue != dsValue) {
|
|
|
@@ -127,7 +98,7 @@ fun <T> prefDelegate(
|
|
|
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
|
|
|
if (currentValue != value) {
|
|
|
updateValue(value)
|
|
|
- // 同步写入 SP(向后兼容:AppConfig/MainViewModel 等仍直接读 SP)
|
|
|
+ // 门面双写:SP(向后兼容,未迁移代码仍直接读 SP/靠 SP 监听)+ 快照/DataStore
|
|
|
when (value) {
|
|
|
is String? -> if (sync) appCtx.putPrefStringSync(key, value) else appCtx.putPrefString(key, value)
|
|
|
is Int -> appCtx.putPrefInt(key, value)
|
|
|
@@ -135,84 +106,29 @@ fun <T> prefDelegate(
|
|
|
is Long -> appCtx.putPrefLong(key, value)
|
|
|
is Float -> appCtx.putPrefFloat(key, value)
|
|
|
}
|
|
|
- // 异步串行写入 DataStore;runBlocking 会把整文件重写 + fsync
|
|
|
- // 挂在调用线程(通常是主线程的点击回调)上,造成明显掉帧
|
|
|
- scope.launch(dsWriteDispatcher) {
|
|
|
- runCatching {
|
|
|
- when (value) {
|
|
|
- is String? -> DsSync.putString(key, value)
|
|
|
- is Int -> DsSync.putInt(key, value)
|
|
|
- is Boolean -> DsSync.putBoolean(key, value)
|
|
|
- is Long -> DsSync.putLong(key, value)
|
|
|
- is Float -> DsSync.putFloat(key, value)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
onValueChange?.invoke(value)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 从 DataStore 读取当前值,DS 读不到时回退到 SP 并补写入 DS。
|
|
|
- */
|
|
|
@Suppress("UNCHECKED_CAST")
|
|
|
- private suspend fun readFromDs(): T? {
|
|
|
- val dsValue = 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
|
|
|
- }
|
|
|
- // DS 有值,直接返回
|
|
|
- if (dsValue != null) return dsValue
|
|
|
- // DS 无值,回退到 SP(迁移遗漏时的补偿)
|
|
|
- val spValue: T? = when {
|
|
|
- defaultValue is String || defaultValue == null ->
|
|
|
- appCtx.getPrefString(key, defaultValue as String?) 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 -> null
|
|
|
- }
|
|
|
- // DS 无值时,将 SP 值补写入 DS(修复迁移遗漏)
|
|
|
- if (spValue != null) {
|
|
|
- runCatching {
|
|
|
- when (spValue) {
|
|
|
- is String? -> DsSync.putString(key, spValue)
|
|
|
- is Int -> DsSync.putInt(key, spValue)
|
|
|
- is Boolean -> DsSync.putBoolean(key, spValue)
|
|
|
- is Long -> DsSync.putLong(key, spValue)
|
|
|
- is Float -> DsSync.putFloat(key, spValue)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- return spValue
|
|
|
+ private fun readInitial(): T = when {
|
|
|
+ defaultValue is String || defaultValue == null ->
|
|
|
+ appCtx.getPrefString(key, defaultValue as String?) 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
|
|
|
+ }
|
|
|
+
|
|
|
+ @Suppress("UNCHECKED_CAST")
|
|
|
+ private fun readTyped(prefs: Preferences): T? = when {
|
|
|
+ defaultValue is String || defaultValue == null -> prefs.compatDsString(key) as T?
|
|
|
+ defaultValue is Int -> prefs.compatDsInt(key) as T?
|
|
|
+ defaultValue is Boolean -> prefs.compatDsBoolean(key) as T?
|
|
|
+ defaultValue is Long -> prefs.compatDsLong(key) as T?
|
|
|
+ defaultValue is Float -> prefs.compatDsFloat(key) as T?
|
|
|
+ else -> null
|
|
|
}
|
|
|
|
|
|
private fun updateValue(value: T) {
|