准备切换至 DataStore 偏好设置管理
This commit is contained in:
3 files changed
+178
-16
No files matched your search
@@ -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.SearchRepository
|
||||||
import io.legado.app.data.repository.SearchRepositoryImpl
|
import io.legado.app.data.repository.SearchRepositoryImpl
|
||||||
import io.legado.app.data.repository.SearchContentRepository
|
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.UploadRepository
|
||||||
import io.legado.app.data.repository.WebDavBackupRepository
|
import io.legado.app.data.repository.WebDavBackupRepository
|
||||||
import io.legado.app.data.repository.WebDavReadingProgressRepository
|
import io.legado.app.data.repository.WebDavReadingProgressRepository
|
||||||
@@ -119,6 +120,7 @@ val appModule = module {
|
|||||||
singleOf(::DictRuleRepository)
|
singleOf(::DictRuleRepository)
|
||||||
singleOf(::SearchContentRepository)
|
singleOf(::SearchContentRepository)
|
||||||
singleOf(::RemoteBookRepository)
|
singleOf(::RemoteBookRepository)
|
||||||
|
singleOf(::SettingsRepository)
|
||||||
singleOf(::ExploreKindUiUseCase)
|
singleOf(::ExploreKindUiUseCase)
|
||||||
singleOf(::AppStartupMaintenanceUseCase)
|
singleOf(::AppStartupMaintenanceUseCase)
|
||||||
singleOf(::BatchCacheDownloadUseCase)
|
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.ReadRecord
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordSession
|
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.DirectLinkUpload
|
||||||
import io.legado.app.help.LauncherIconHelp
|
import io.legado.app.help.LauncherIconHelp
|
||||||
import io.legado.app.help.book.isLocal
|
import io.legado.app.help.book.isLocal
|
||||||
@@ -57,6 +58,8 @@ import kotlinx.coroutines.delay
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
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 val mutex = Mutex()
|
||||||
|
|
||||||
private const val TAG = "Restore"
|
private const val TAG = "Restore"
|
||||||
@@ -306,36 +310,40 @@ object Restore {
|
|||||||
}
|
}
|
||||||
//AppWebDav.downBgs()
|
//AppWebDav.downBgs()
|
||||||
appCtx.getSharedPreferences(path, "config")?.all?.let { map ->
|
appCtx.getSharedPreferences(path, "config")?.all?.let { map ->
|
||||||
|
val finalMap = mutableMapOf<String, Any>()
|
||||||
appCtx.defaultSharedPreferences.edit {
|
appCtx.defaultSharedPreferences.edit {
|
||||||
|
|
||||||
map.forEach { (key, value) ->
|
map.forEach { (key, value) ->
|
||||||
if (BackupConfig.keyIsNotIgnore(key)) {
|
if (BackupConfig.keyIsNotIgnore(key)) {
|
||||||
when (key) {
|
when (key) {
|
||||||
PreferKey.webDavPassword -> {
|
PreferKey.webDavPassword -> {
|
||||||
kotlin.runCatching {
|
val password = kotlin.runCatching {
|
||||||
aes.decryptStr(value.toString())
|
aes.decryptStr(value.toString())
|
||||||
}.getOrNull()?.let {
|
}.getOrNull() ?: let {
|
||||||
|
if (appCtx.getPrefString(PreferKey.webDavPassword).isNullOrBlank()) {
|
||||||
|
value.toString()
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
password?.let {
|
||||||
putString(key, it)
|
putString(key, it)
|
||||||
} ?: let {
|
finalMap[key] = it
|
||||||
if (appCtx.getPrefString(PreferKey.webDavPassword)
|
|
||||||
.isNullOrBlank()
|
|
||||||
) {
|
|
||||||
putString(key, value.toString())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> when (value) {
|
else -> {
|
||||||
is Int -> putInt(key, value)
|
when (value) {
|
||||||
is Boolean -> putBoolean(key, value)
|
is Int -> { putInt(key, value); finalMap[key] = value }
|
||||||
is Long -> putLong(key, value)
|
is Boolean -> { putBoolean(key, value); finalMap[key] = value }
|
||||||
is Float -> putFloat(key, value)
|
is Long -> { putLong(key, value); finalMap[key] = value }
|
||||||
is String -> putString(key, value)
|
is Float -> { putFloat(key, value); finalMap[key] = value }
|
||||||
|
is String -> { putString(key, value); finalMap[key] = value }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 同步恢复到 DataStore
|
||||||
|
settingsRepository.batchPutFromMap(finalMap)
|
||||||
}
|
}
|
||||||
ReadBookConfig.apply {
|
ReadBookConfig.apply {
|
||||||
comicStyleSelect = appCtx.getPrefInt(PreferKey.comicStyleSelect)
|
comicStyleSelect = appCtx.getPrefInt(PreferKey.comicStyleSelect)
|
||||||
|
|||||||
Reference in new issue
Block a user