mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-25 18:47:51 +02:00
Use 1.x preference abstraction (#8020)
* Use 1.x preference abstraction - Uses SharedPreferences compared to 1.x impl which uses DataStore but it breaks all settings screens currently - Move PreferencesHelper to new PreferenceStore - PreferencesHelper should be split into smaller preference stores and be in core or domain - Remove flow preferences as new PreferenceStore handles changes for us Co-authored-by: inorichi <3521738+inorichi@users.noreply.github.com> * Fix PreferenceMutableState not updating * Fix changes not emitting on first subscription Co-authored-by: inorichi <3521738+inorichi@users.noreply.github.com>
This commit is contained in:
@ -0,0 +1,178 @@
|
||||
package eu.kanade.tachiyomi.core.preference
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.Editor
|
||||
import androidx.core.content.edit
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
sealed class AndroidPreference<T>(
|
||||
private val preferences: SharedPreferences,
|
||||
private val keyFlow: Flow<String?>,
|
||||
private val key: String,
|
||||
private val defaultValue: T,
|
||||
) : Preference<T> {
|
||||
|
||||
abstract fun read(preferences: SharedPreferences, key: String, defaultValue: T): T
|
||||
|
||||
abstract fun write(key: String, value: T): Editor.() -> Unit
|
||||
|
||||
override fun key(): String {
|
||||
return key
|
||||
}
|
||||
|
||||
override fun get(): T {
|
||||
return read(preferences, key, defaultValue)
|
||||
}
|
||||
|
||||
override fun set(value: T) {
|
||||
preferences.edit(action = write(key, value))
|
||||
}
|
||||
|
||||
override fun isSet(): Boolean {
|
||||
return preferences.contains(key)
|
||||
}
|
||||
|
||||
override fun delete() {
|
||||
preferences.edit {
|
||||
remove(key)
|
||||
}
|
||||
}
|
||||
|
||||
override fun defaultValue(): T {
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
override fun changes(): Flow<T> {
|
||||
return keyFlow
|
||||
.filter { it == key || it == null }
|
||||
.onStart { emit("ignition") }
|
||||
.map { get() }
|
||||
.conflate()
|
||||
}
|
||||
|
||||
override fun stateIn(scope: CoroutineScope): StateFlow<T> {
|
||||
return changes().stateIn(scope, SharingStarted.Eagerly, get())
|
||||
}
|
||||
|
||||
class StringPrimitive(
|
||||
preferences: SharedPreferences,
|
||||
keyFlow: Flow<String?>,
|
||||
key: String,
|
||||
defaultValue: String
|
||||
) : AndroidPreference<String>(preferences, keyFlow, key, defaultValue) {
|
||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: String): String {
|
||||
return preferences.getString(key, defaultValue) ?: defaultValue
|
||||
}
|
||||
|
||||
override fun write(key: String, value: String): Editor.() -> Unit = {
|
||||
putString(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
class LongPrimitive(
|
||||
preferences: SharedPreferences,
|
||||
keyFlow: Flow<String?>,
|
||||
key: String,
|
||||
defaultValue: Long
|
||||
) : AndroidPreference<Long>(preferences, keyFlow, key, defaultValue) {
|
||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: Long): Long {
|
||||
return preferences.getLong(key, defaultValue)
|
||||
}
|
||||
|
||||
override fun write(key: String, value: Long): Editor.() -> Unit = {
|
||||
putLong(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
class IntPrimitive(
|
||||
preferences: SharedPreferences,
|
||||
keyFlow: Flow<String?>,
|
||||
key: String,
|
||||
defaultValue: Int
|
||||
) : AndroidPreference<Int>(preferences, keyFlow, key, defaultValue) {
|
||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: Int): Int {
|
||||
return preferences.getInt(key, defaultValue)
|
||||
}
|
||||
|
||||
override fun write(key: String, value: Int): Editor.() -> Unit = {
|
||||
putInt(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
class FloatPrimitive(
|
||||
preferences: SharedPreferences,
|
||||
keyFlow: Flow<String?>,
|
||||
key: String,
|
||||
defaultValue: Float
|
||||
) : AndroidPreference<Float>(preferences, keyFlow, key, defaultValue) {
|
||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: Float): Float {
|
||||
return preferences.getFloat(key, defaultValue)
|
||||
}
|
||||
|
||||
override fun write(key: String, value: Float): Editor.() -> Unit = {
|
||||
putFloat(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
class BooleanPrimitive(
|
||||
preferences: SharedPreferences,
|
||||
keyFlow: Flow<String?>,
|
||||
key: String,
|
||||
defaultValue: Boolean
|
||||
) : AndroidPreference<Boolean>(preferences, keyFlow, key, defaultValue) {
|
||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: Boolean): Boolean {
|
||||
return preferences.getBoolean(key, defaultValue)
|
||||
}
|
||||
|
||||
override fun write(key: String, value: Boolean): Editor.() -> Unit = {
|
||||
putBoolean(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
class StringSetPrimitive(
|
||||
preferences: SharedPreferences,
|
||||
keyFlow: Flow<String?>,
|
||||
key: String,
|
||||
defaultValue: Set<String>
|
||||
) : AndroidPreference<Set<String>>(preferences, keyFlow, key, defaultValue) {
|
||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: Set<String>): Set<String> {
|
||||
return preferences.getStringSet(key, defaultValue) ?: defaultValue
|
||||
}
|
||||
|
||||
override fun write(key: String, value: Set<String>): Editor.() -> Unit = {
|
||||
putStringSet(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
class Object<T>(
|
||||
preferences: SharedPreferences,
|
||||
keyFlow: Flow<String?>,
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
val serializer: (T) -> String,
|
||||
val deserializer: (String) -> T
|
||||
) : AndroidPreference<T>(preferences, keyFlow, key, defaultValue) {
|
||||
override fun read(preferences: SharedPreferences, key: String, defaultValue: T): T {
|
||||
return try {
|
||||
preferences.getString(key, null)?.let(deserializer) ?: defaultValue
|
||||
} catch (e: Exception) {
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(key: String, value: T): Editor.() -> Unit = {
|
||||
putString(key, serializer(value))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
package eu.kanade.tachiyomi.core.preference
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import eu.kanade.tachiyomi.core.preference.AndroidPreference.BooleanPrimitive
|
||||
import eu.kanade.tachiyomi.core.preference.AndroidPreference.FloatPrimitive
|
||||
import eu.kanade.tachiyomi.core.preference.AndroidPreference.IntPrimitive
|
||||
import eu.kanade.tachiyomi.core.preference.AndroidPreference.LongPrimitive
|
||||
import eu.kanade.tachiyomi.core.preference.AndroidPreference.Object
|
||||
import eu.kanade.tachiyomi.core.preference.AndroidPreference.StringPrimitive
|
||||
import eu.kanade.tachiyomi.core.preference.AndroidPreference.StringSetPrimitive
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
|
||||
class AndroidPreferenceStore(
|
||||
context: Context
|
||||
) : PreferenceStore {
|
||||
|
||||
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
private val keyFlow = sharedPreferences.keyFlow
|
||||
|
||||
override fun getString(key: String, defaultValue: String): Preference<String> {
|
||||
return StringPrimitive(sharedPreferences, keyFlow, key, defaultValue)
|
||||
}
|
||||
|
||||
override fun getLong(key: String, defaultValue: Long): Preference<Long> {
|
||||
return LongPrimitive(sharedPreferences, keyFlow,key, defaultValue)
|
||||
}
|
||||
|
||||
override fun getInt(key: String, defaultValue: Int): Preference<Int> {
|
||||
return IntPrimitive(sharedPreferences, keyFlow,key, defaultValue)
|
||||
}
|
||||
|
||||
override fun getFloat(key: String, defaultValue: Float): Preference<Float> {
|
||||
return FloatPrimitive(sharedPreferences, keyFlow,key, defaultValue)
|
||||
}
|
||||
|
||||
override fun getBoolean(key: String, defaultValue: Boolean): Preference<Boolean> {
|
||||
return BooleanPrimitive(sharedPreferences, keyFlow, key, defaultValue)
|
||||
}
|
||||
|
||||
override fun getStringSet(key: String, defaultValue: Set<String>): Preference<Set<String>> {
|
||||
return StringSetPrimitive(sharedPreferences, keyFlow, key, defaultValue)
|
||||
}
|
||||
|
||||
override fun <T> getObject(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
serializer: (T) -> String,
|
||||
deserializer: (String) -> T,
|
||||
): Preference<T> {
|
||||
return Object(
|
||||
preferences = sharedPreferences,
|
||||
keyFlow = keyFlow,
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
serializer = serializer,
|
||||
deserializer = deserializer
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val SharedPreferences.keyFlow
|
||||
get() = callbackFlow {
|
||||
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? -> trySend(key) }
|
||||
registerOnSharedPreferenceChangeListener(listener)
|
||||
awaitClose {
|
||||
unregisterOnSharedPreferenceChangeListener(listener)
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package eu.kanade.tachiyomi.core.preference
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface Preference<T> {
|
||||
|
||||
fun key(): String
|
||||
|
||||
fun get(): T
|
||||
|
||||
fun set(value: T)
|
||||
|
||||
fun isSet(): Boolean
|
||||
|
||||
fun delete()
|
||||
|
||||
fun defaultValue(): T
|
||||
|
||||
fun changes(): Flow<T>
|
||||
|
||||
fun stateIn(scope: CoroutineScope): StateFlow<T>
|
||||
|
||||
}
|
||||
|
||||
inline fun <reified T, R : T> Preference<T>.getAndSet(crossinline block: (T) -> R) = set(block(get()))
|
@ -0,0 +1,42 @@
|
||||
package eu.kanade.tachiyomi.core.preference
|
||||
|
||||
interface PreferenceStore {
|
||||
|
||||
fun getString(key: String, defaultValue: String = ""): Preference<String>
|
||||
|
||||
fun getLong(key: String, defaultValue: Long = 0): Preference<Long>
|
||||
|
||||
fun getInt(key: String, defaultValue: Int = 0): Preference<Int>
|
||||
|
||||
fun getFloat(key: String, defaultValue: Float = 0f): Preference<Float>
|
||||
|
||||
fun getBoolean(key: String, defaultValue: Boolean = false): Preference<Boolean>
|
||||
|
||||
fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Preference<Set<String>>
|
||||
|
||||
fun <T> getObject(
|
||||
key: String,
|
||||
defaultValue: T,
|
||||
serializer: (T) -> String,
|
||||
deserializer: (String) -> T
|
||||
): Preference<T>
|
||||
|
||||
}
|
||||
|
||||
inline fun <reified T : Enum<T>> PreferenceStore.getEnum(
|
||||
key: String,
|
||||
defaultValue: T
|
||||
) : Preference<T> {
|
||||
return getObject(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
serializer = { it.name },
|
||||
deserializer = {
|
||||
try {
|
||||
enumValueOf(it)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
@ -8,13 +8,13 @@ import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
|
||||
import okhttp3.Cache
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class NetworkHelper(context: Context) {
|
||||
|
||||
// TODO: Abstract preferences similar to 1.x
|
||||
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val preferences: NetworkPreferences by injectLazy()
|
||||
|
||||
private val cacheDir = File(context.cacheDir, "network_cache")
|
||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||
@ -36,14 +36,14 @@ class NetworkHelper(context: Context) {
|
||||
.addInterceptor(userAgentInterceptor)
|
||||
.addNetworkInterceptor(http103Interceptor)
|
||||
|
||||
if (preferences.getBoolean("verbose_logging", false)) {
|
||||
if (preferences.verboseLogging().get()) {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.HEADERS
|
||||
}
|
||||
builder.addNetworkInterceptor(httpLoggingInterceptor)
|
||||
}
|
||||
|
||||
when (preferences.getInt("doh_provider", -1)) {
|
||||
when (preferences.dohProvider().get()) {
|
||||
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||
PREF_DOH_GOOGLE -> builder.dohGoogle()
|
||||
PREF_DOH_ADGUARD -> builder.dohAdGuard()
|
||||
@ -70,6 +70,6 @@ class NetworkHelper(context: Context) {
|
||||
}
|
||||
|
||||
val defaultUserAgent by lazy {
|
||||
preferences.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0")!!
|
||||
preferences.defaultUserAgent().get()
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
package eu.kanade.tachiyomi.network
|
||||
|
||||
import eu.kanade.tachiyomi.core.preference.Preference
|
||||
import eu.kanade.tachiyomi.core.preference.PreferenceStore
|
||||
|
||||
class NetworkPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
private val verboseLogging: Boolean = false
|
||||
) {
|
||||
|
||||
fun verboseLogging(): Preference<Boolean> {
|
||||
return preferenceStore.getBoolean("verbose_logging", verboseLogging)
|
||||
}
|
||||
|
||||
fun dohProvider(): Preference<Int> {
|
||||
return preferenceStore.getInt("doh_provider", -1)
|
||||
}
|
||||
|
||||
fun defaultUserAgent(): Preference<String> {
|
||||
return preferenceStore.getString("default_user_agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0")
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user