准备切换至 DataStore 偏好设置管理

This commit is contained in:
HapeLee
2026-05-14 15:59:58 +08:00
parent 7fda394d74
commit 16353d8636
3 changed files with 178 additions and 16 deletions
@@ -0,0 +1,152 @@
package io.legado.app.data.repository
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import io.legado.app.utils.putPrefBoolean
import io.legado.app.utils.putPrefFloat
import io.legado.app.utils.putPrefInt
import io.legado.app.utils.putPrefLong
import io.legado.app.utils.putPrefString
import io.legado.app.utils.putPrefStringSet
import io.legado.app.utils.removePref
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings",
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(
context,
"${context.packageName}_preferences"
)
)
}
)
/**
* 设置仓储
* 采用 DataStore + SharedPreferences 桥接模式
* 写入时同时写入两者,读取时以 DataStore 为准
* 这样可以保持现有的基于 SP 的备份恢复功能正常工作
*/
class SettingsRepository(private val context: Context) {
private val dataStore = context.dataStore
fun <T> getPreference(key: Preferences.Key<T>, defaultValue: T): Flow<T> {
return dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}
.map { preferences ->
preferences[key] ?: defaultValue
}
}
suspend fun <T> updatePreference(key: Preferences.Key<T>, value: T) {
// 1. 写入 DataStore (UI 响应式来源)
dataStore.edit { preferences ->
preferences[key] = value
}
// 2. 桥接:写回 SharedPreferences (保证备份功能正常)
syncToSharedPrefs(key.name, value)
}
private fun syncToSharedPrefs(key: String, value: Any?) {
when (value) {
is String -> context.putPrefString(key, value)
is Int -> context.putPrefInt(key, value)
is Boolean -> context.putPrefBoolean(key, value)
is Long -> context.putPrefLong(key, value)
is Float -> context.putPrefFloat(key, value)
is Set<*> -> {
@Suppress("UNCHECKED_CAST")
context.putPrefStringSet(key, value as MutableSet<String>)
}
null -> context.removePref(key)
}
}
// String 类型的快捷访问
fun getString(key: String, defaultValue: String = ""): Flow<String> =
getPreference(stringPreferencesKey(key), defaultValue)
suspend fun putString(key: String, value: String) =
updatePreference(stringPreferencesKey(key), value)
// Int 类型的快捷访问
fun getInt(key: String, defaultValue: Int = 0): Flow<Int> =
getPreference(intPreferencesKey(key), defaultValue)
suspend fun putInt(key: String, value: Int) =
updatePreference(intPreferencesKey(key), value)
// Boolean 类型的快捷访问
fun getBoolean(key: String, defaultValue: Boolean = false): Flow<Boolean> =
getPreference(booleanPreferencesKey(key), defaultValue)
suspend fun putBoolean(key: String, value: Boolean) =
updatePreference(booleanPreferencesKey(key), value)
// Long 类型的快捷访问
fun getLong(key: String, defaultValue: Long = 0L): Flow<Long> =
getPreference(longPreferencesKey(key), defaultValue)
suspend fun putLong(key: String, value: Long) =
updatePreference(longPreferencesKey(key), value)
// Float 类型的快捷访问
fun getFloat(key: String, defaultValue: Float = 0f): Flow<Float> =
getPreference(floatPreferencesKey(key), defaultValue)
suspend fun putFloat(key: String, value: Float) =
updatePreference(floatPreferencesKey(key), value)
// Set<String> 类型的快捷访问
fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Flow<Set<String>> =
getPreference(stringSetPreferencesKey(key), defaultValue)
suspend fun putStringSet(key: String, value: Set<String>) =
updatePreference(stringSetPreferencesKey(key), value)
// 批量从 Map 恢复到 DataStore (用于兼容 Restore 逻辑)
suspend fun batchPutFromMap(map: Map<String, *>) {
dataStore.edit { preferences ->
map.forEach { (key, value) ->
when (value) {
is String -> preferences[stringPreferencesKey(key)] = value
is Int -> preferences[intPreferencesKey(key)] = value
is Boolean -> preferences[booleanPreferencesKey(key)] = value
is Long -> preferences[longPreferencesKey(key)] = value
is Float -> preferences[floatPreferencesKey(key)] = value
is Set<*> -> {
@Suppress("UNCHECKED_CAST")
preferences[stringSetPreferencesKey(key)] = value as Set<String>
}
}
}
}
}
// 移除配置
suspend fun remove(key: String) {
dataStore.edit { preferences ->
preferences.remove(stringPreferencesKey(key))
preferences.remove(intPreferencesKey(key))
preferences.remove(booleanPreferencesKey(key))
preferences.remove(longPreferencesKey(key))
preferences.remove(floatPreferencesKey(key))
}
context.removePref(key)
}
}
@@ -25,6 +25,7 @@ import io.legado.app.data.repository.RssRepository
import io.legado.app.data.repository.SearchRepository
import io.legado.app.data.repository.SearchRepositoryImpl
import io.legado.app.data.repository.SearchContentRepository
import io.legado.app.data.repository.SettingsRepository
import io.legado.app.data.repository.UploadRepository
import io.legado.app.data.repository.WebDavBackupRepository
import io.legado.app.data.repository.WebDavReadingProgressRepository
@@ -119,6 +120,7 @@ val appModule = module {
singleOf(::DictRuleRepository)
singleOf(::SearchContentRepository)
singleOf(::RemoteBookRepository)
singleOf(::SettingsRepository)
singleOf(::ExploreKindUiUseCase)
singleOf(::AppStartupMaintenanceUseCase)
singleOf(::BatchCacheDownloadUseCase)
@@ -28,6 +28,7 @@ import io.legado.app.data.entities.TxtTocRule
import io.legado.app.data.entities.readRecord.ReadRecord
import io.legado.app.data.entities.readRecord.ReadRecordDetail
import io.legado.app.data.entities.readRecord.ReadRecordSession
import io.legado.app.data.repository.SettingsRepository
import io.legado.app.help.DirectLinkUpload
import io.legado.app.help.LauncherIconHelp
import io.legado.app.help.book.isLocal
@@ -57,6 +58,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import splitties.init.appCtx
import java.io.File
import java.io.FileInputStream
@@ -64,8 +67,9 @@ import java.io.FileInputStream
/**
* 恢复
*/
object Restore {
object Restore : KoinComponent {
private val settingsRepository: SettingsRepository by inject()
private val mutex = Mutex()
private const val TAG = "Restore"
@@ -306,36 +310,40 @@ object Restore {
}
//AppWebDav.downBgs()
appCtx.getSharedPreferences(path, "config")?.all?.let { map ->
val finalMap = mutableMapOf<String, Any>()
appCtx.defaultSharedPreferences.edit {
map.forEach { (key, value) ->
if (BackupConfig.keyIsNotIgnore(key)) {
when (key) {
PreferKey.webDavPassword -> {
kotlin.runCatching {
val password = kotlin.runCatching {
aes.decryptStr(value.toString())
}.getOrNull()?.let {
}.getOrNull() ?: let {
if (appCtx.getPrefString(PreferKey.webDavPassword).isNullOrBlank()) {
value.toString()
} else null
}
password?.let {
putString(key, it)
} ?: let {
if (appCtx.getPrefString(PreferKey.webDavPassword)
.isNullOrBlank()
) {
putString(key, value.toString())
}
finalMap[key] = it
}
}
else -> when (value) {
is Int -> putInt(key, value)
is Boolean -> putBoolean(key, value)
is Long -> putLong(key, value)
is Float -> putFloat(key, value)
is String -> putString(key, value)
else -> {
when (value) {
is Int -> { putInt(key, value); finalMap[key] = value }
is Boolean -> { putBoolean(key, value); finalMap[key] = value }
is Long -> { putLong(key, value); finalMap[key] = value }
is Float -> { putFloat(key, value); finalMap[key] = value }
is String -> { putString(key, value); finalMap[key] = value }
}
}
}
}
}
}
// 同步恢复到 DataStore
settingsRepository.batchPutFromMap(finalMap)
}
ReadBookConfig.apply {
comicStyleSelect = appCtx.getPrefInt(PreferKey.comicStyleSelect)