Backup App and Source settings

Also added PreferenceStore/TrackPreferences and made the track username/password private
Updated kotlinSerialization to 1.6.0

Co-Authored-By: jmir1 <43830312+jmir1@users.noreply.github.com>
Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com>
This commit is contained in:
Jays2Kings 2023-10-09 14:08:36 -07:00
parent cbd6003aba
commit 106f6f52c0
29 changed files with 675 additions and 78 deletions

View file

@ -203,7 +203,7 @@ dependencies {
implementation(kotlin("reflect", version = AndroidVersions.kotlin))
// JSON
val kotlinSerialization = "1.5.1"
val kotlinSerialization = "1.6.0"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${kotlinSerialization}")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:${kotlinSerialization}")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:${kotlinSerialization}")

View file

@ -7,8 +7,11 @@ import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.data.preference.AndroidPreferenceStore
import eu.kanade.tachiyomi.data.preference.PreferenceStore
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackPreferences
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.JavaScriptEngine
import eu.kanade.tachiyomi.network.NetworkHelper
@ -27,8 +30,14 @@ class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)
addSingletonFactory<PreferenceStore> {
AndroidPreferenceStore(app)
}
addSingletonFactory { PreferencesHelper(app) }
addSingletonFactory { TrackPreferences(get()) }
addSingletonFactory { DatabaseHelper(app) }
addSingletonFactory { ChapterCache(app) }

View file

@ -5,7 +5,9 @@ import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.Preference
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.PreferenceStore
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.plusAssign
@ -33,7 +35,11 @@ object Migrations {
* @param preferences Preferences of the application.
* @return true if a migration is performed, false otherwise.
*/
fun upgrade(preferences: PreferencesHelper, scope: CoroutineScope): Boolean {
fun upgrade(
preferences: PreferencesHelper,
preferenceStore: PreferenceStore,
scope: CoroutineScope,
): Boolean {
val context = preferences.context
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
prefs.edit {
@ -220,6 +226,19 @@ object Migrations {
LibraryUpdateJob.cancelAllWorks(context)
LibraryUpdateJob.setupTask(context)
}
if (oldVersion < 108) {
preferenceStore.getAll()
.filter { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") }
.forEach { (key, value) ->
if (value is String) {
preferenceStore
.getString(Preference.privateKey(key))
.set(value)
preferenceStore.getString(key).delete()
}
}
}
return true
}

View file

@ -8,17 +8,22 @@ object BackupConst {
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
// Filter options
internal const val BACKUP_CATEGORY = 0x1
internal const val BACKUP_CATEGORY_MASK = 0x1
internal const val BACKUP_CHAPTER = 0x2
internal const val BACKUP_CHAPTER_MASK = 0x2
internal const val BACKUP_HISTORY = 0x4
internal const val BACKUP_HISTORY_MASK = 0x4
internal const val BACKUP_TRACK = 0x8
internal const val BACKUP_TRACK_MASK = 0x8
internal const val BACKUP_CUSTOM_INFO = 0x10
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
internal const val BACKUP_READ_MANGA = 0x20
internal const val BACKUP_READ_MANGA_MASK = 0x20
internal const val BACKUP_ALL = 0x1F
const val BACKUP_CATEGORY = 0x1
const val BACKUP_CATEGORY_MASK = 0x1
const val BACKUP_CHAPTER = 0x2
const val BACKUP_CHAPTER_MASK = 0x2
const val BACKUP_HISTORY = 0x4
const val BACKUP_HISTORY_MASK = 0x4
const val BACKUP_TRACK = 0x8
const val BACKUP_TRACK_MASK = 0x8
const val BACKUP_APP_PREFS = 0x10
const val BACKUP_APP_PREFS_MASK = 0x10
const val BACKUP_SOURCE_PREFS = 0x20
const val BACKUP_SOURCE_PREFS_MASK = 0x20
const val BACKUP_CUSTOM_INFO = 0x40
const val BACKUP_CUSTOM_INFO_MASK = 0x40
const val BACKUP_READ_MANGA = 0x80
const val BACKUP_READ_MANGA_MASK = 0x80
const val BACKUP_ALL = 0x7F
}

View file

@ -4,6 +4,8 @@ import android.content.Context
import android.net.Uri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
@ -14,6 +16,8 @@ import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_READ_MANGA
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_READ_MANGA_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS_MASK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
import eu.kanade.tachiyomi.data.backup.models.Backup
@ -21,25 +25,41 @@ import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
import eu.kanade.tachiyomi.data.backup.models.BackupSource
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
import eu.kanade.tachiyomi.data.backup.models.BackupTracking
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.Preference
import eu.kanade.tachiyomi.data.preference.PreferenceStore
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.preferenceKey
import eu.kanade.tachiyomi.source.sourcePreferences
import kotlinx.serialization.protobuf.ProtoBuf
import okio.buffer
import okio.gzip
import okio.sink
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.FileOutputStream
import kotlin.math.max
class BackupManager(context: Context) : AbstractBackupManager(context) {
private val preferenceStore: PreferenceStore = Injekt.get()
val parser = ProtoBuf
/**
@ -64,6 +84,8 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
backupCategories(),
emptyList(),
backupExtensionInfo(databaseManga),
backupAppPreferences(flags),
backupSourcePreferences(flags),
)
}
@ -198,6 +220,41 @@ class BackupManager(context: Context) : AbstractBackupManager(context) {
return mangaObject
}
private fun backupAppPreferences(flags: Int): List<BackupPreference> {
if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList()
return preferenceStore.getAll().toBackupPreferences()
}
private fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList()
return sourceManager.getOnlineSources()
.filterIsInstance<ConfigurableSource>()
.map {
BackupSourcePreferences(
it.preferenceKey(),
it.sourcePreferences().all.toBackupPreferences(),
)
}
}
@Suppress("UNCHECKED_CAST")
private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
return this.filterKeys { !Preference.isPrivate(it) }
.mapNotNull { (key, value) ->
when (value) {
is Int -> BackupPreference(key, IntPreferenceValue(value))
is Long -> BackupPreference(key, LongPreferenceValue(value))
is Float -> BackupPreference(key, FloatPreferenceValue(value))
is String -> BackupPreference(key, StringPreferenceValue(value))
is Boolean -> BackupPreference(key, BooleanPreferenceValue(value))
is Set<*> -> (value as? Set<String>)?.let {
BackupPreference(key, StringSetPreferenceValue(it))
}
else -> null
}
}
}
fun restoreExistingManga(manga: Manga, dbManga: Manga) {
manga.id = dbManga.id
manga.copyFrom(dbManga)

View file

@ -7,24 +7,38 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
import eu.kanade.tachiyomi.data.backup.models.BackupSource
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.AndroidPreferenceStore
import eu.kanade.tachiyomi.data.preference.PreferenceStore
import eu.kanade.tachiyomi.source.sourcePreferences
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
import okio.buffer
import okio.gzip
import okio.source
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
class BackupRestorer(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<BackupManager>(context, notifier) {
private val preferenceStore: PreferenceStore = Injekt.get()
@SuppressLint("Recycle")
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun performRestore(uri: Uri): Boolean {
backupManager = BackupManager(context)
@ -44,6 +58,9 @@ class BackupRestorer(context: Context, notifier: BackupNotifier) : AbstractBacku
sourceMapping = backupMaps.associate { it.sourceId to it.name }
return coroutineScope {
restoreAppPreferences(backup.backupPreferences)
restoreSourcePreferences(backup.backupSourcePreferences)
// Restore individual manga
backup.backupManga.forEach {
if (!isActive) {
@ -147,4 +164,56 @@ class BackupRestorer(context: Context, notifier: BackupNotifier) : AbstractBacku
customManga?.id = manga.id!!
customManga?.let { customMangaManager.saveMangaInfo(it) }
}
private fun restoreAppPreferences(preferences: List<BackupPreference>) {
restorePreferences(preferences, preferenceStore)
}
private fun restoreSourcePreferences(preferences: List<BackupSourcePreferences>) {
preferences.forEach {
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
restorePreferences(it.prefs, sourcePrefs)
}
}
private fun restorePreferences(
toRestore: List<BackupPreference>,
preferenceStore: PreferenceStore,
) {
val prefs = preferenceStore.getAll()
toRestore.forEach { (key, value) ->
when (value) {
is IntPreferenceValue -> {
if (prefs[key] is Int?) {
preferenceStore.getInt(key).set(value.value)
}
}
is LongPreferenceValue -> {
if (prefs[key] is Long?) {
preferenceStore.getLong(key).set(value.value)
}
}
is FloatPreferenceValue -> {
if (prefs[key] is Float?) {
preferenceStore.getFloat(key).set(value.value)
}
}
is StringPreferenceValue -> {
if (prefs[key] is String?) {
preferenceStore.getString(key).set(value.value)
}
}
is BooleanPreferenceValue -> {
if (prefs[key] is Boolean?) {
preferenceStore.getBoolean(key).set(value.value)
}
}
is StringSetPreferenceValue -> {
if (prefs[key] is Set<*>?) {
preferenceStore.getStringSet(key).set(value.value)
}
}
}
}
}
}

View file

@ -11,10 +11,12 @@ import java.util.Locale
data class Backup(
@ProtoNumber(1) val backupManga: List<BackupManga>,
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
// Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var backupBrokenSources: List<BrokenBackupSource> = emptyList(),
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(),
) {
companion object {
val filenameRegex = """(${BuildConfig.APPLICATION_ID}|tachiyomi)?_\d+-\d+-\d+_\d+-\d+\.(tachibk|proto\.gz)""".toRegex()

View file

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.data.backup.models
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BackupPreference(
@ProtoNumber(1) val key: String,
@ProtoNumber(2) val value: PreferenceValue,
)
@Serializable
data class BackupSourcePreferences(
@ProtoNumber(1) val sourceKey: String,
@ProtoNumber(2) val prefs: List<BackupPreference>,
)
@Serializable
sealed class PreferenceValue
@Serializable
data class IntPreferenceValue(val value: Int) : PreferenceValue()
@Serializable
data class LongPreferenceValue(val value: Long) : PreferenceValue()
@Serializable
data class FloatPreferenceValue(val value: Float) : PreferenceValue()
@Serializable
data class StringPreferenceValue(val value: String) : PreferenceValue()
@Serializable
data class BooleanPreferenceValue(val value: Boolean) : PreferenceValue()
@Serializable
data class StringSetPreferenceValue(val value: Set<String>) : PreferenceValue()

View file

@ -0,0 +1,194 @@
package eu.kanade.tachiyomi.data.preference
import android.content.SharedPreferences
import android.content.SharedPreferences.Editor
import androidx.core.content.edit
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.onStart
import kotlinx.coroutines.flow.stateIn
import timber.log.Timber
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 try {
read(preferences, key, defaultValue)
} catch (e: ClassCastException) {
Timber.d("Invalid value for $key; deleting")
delete()
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))
}
}
}

View file

@ -0,0 +1,79 @@
package eu.kanade.tachiyomi.data.preference
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.data.preference.AndroidPreference.BooleanPrimitive
import eu.kanade.tachiyomi.data.preference.AndroidPreference.FloatPrimitive
import eu.kanade.tachiyomi.data.preference.AndroidPreference.IntPrimitive
import eu.kanade.tachiyomi.data.preference.AndroidPreference.LongPrimitive
import eu.kanade.tachiyomi.data.preference.AndroidPreference.Object
import eu.kanade.tachiyomi.data.preference.AndroidPreference.StringPrimitive
import eu.kanade.tachiyomi.data.preference.AndroidPreference.StringSetPrimitive
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
class AndroidPreferenceStore(
context: Context,
private val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context),
) : PreferenceStore {
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,
)
}
override fun getAll(): Map<String, *> {
return sharedPreferences.all ?: emptyMap<String, Any>()
}
}
private val SharedPreferences.keyFlow
get() = callbackFlow {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key: String? ->
trySend(
key,
)
}
registerOnSharedPreferenceChangeListener(listener)
awaitClose {
unregisterOnSharedPreferenceChangeListener(listener)
}
}

View file

@ -0,0 +1,59 @@
package eu.kanade.tachiyomi.data.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>
val isPrivate: Boolean
get() = key().startsWith(PRIVATE_PREFIX)
companion object {
/**
* A preference that should not be exposed in places like backups.
*/
fun isPrivate(key: String): Boolean {
return key.startsWith(PRIVATE_PREFIX)
}
fun privateKey(key: String): String {
return "$PRIVATE_PREFIX$key"
}
private const val PRIVATE_PREFIX = "__PRIVATE_"
}
}
inline fun <reified T, R : T> Preference<T>.getAndSet(crossinline block: (T) -> R) = set(
block(get()),
)
operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
set(get() + item)
}
operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
set(get() - item)
}
fun Preference<Boolean>.toggle(): Boolean {
set(!get())
return get()
}

View file

@ -241,10 +241,4 @@ object PreferenceKeys {
const val hideChapterTitles = "hide_chapter_titles"
const val chaptersDescAsDefault = "chapters_desc_as_default"
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
fun trackToken(syncId: Int) = "track_token_$syncId"
}

View file

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.data.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>
fun getAll(): Map<String, *>
}
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
}
},
)
}

View file

@ -11,7 +11,6 @@ import com.google.android.material.color.DynamicColors
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.updater.AppDownloadInstallJob
import eu.kanade.tachiyomi.extension.model.InstalledExtensionsOrder
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller
@ -223,19 +222,6 @@ class PreferencesHelper(val context: Context) {
fun sourceSorting() = flowPrefs.getInt(Keys.sourcesSort, 0)
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
fun trackPassword(sync: TrackService) = prefs.getString(Keys.trackPassword(sync.id), "")
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
prefs.edit()
.putString(Keys.trackUsername(sync.id), username)
.putString(Keys.trackPassword(sync.id), password)
.apply()
}
fun trackToken(sync: TrackService) = flowPrefs.getString(Keys.trackToken(sync.id), "")
fun anilistScoreType() = flowPrefs.getString("anilist_score_type", "POINT_10")
fun backupsDirectory() = flowPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())

View file

@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.data.track
import eu.kanade.tachiyomi.data.preference.Preference
import eu.kanade.tachiyomi.data.preference.PreferenceStore
import eu.kanade.tachiyomi.data.track.anilist.Anilist
class TrackPreferences(
private val preferenceStore: PreferenceStore,
) {
fun trackUsername(sync: TrackService) = preferenceStore.getString(trackUsername(sync.id), "")
fun trackPassword(sync: TrackService) = preferenceStore.getString(trackPassword(sync.id), "")
fun setCredentials(sync: TrackService, username: String, password: String) {
trackUsername(sync).set(username)
trackPassword(sync).set(password)
}
fun trackToken(sync: TrackService) = preferenceStore.getString(trackToken(sync.id), "")
fun anilistScoreType() = preferenceStore.getString("anilist_score_type", Anilist.POINT_10)
fun autoUpdateTrack() = preferenceStore.getBoolean("pref_auto_update_manga_sync_key", true)
companion object {
fun trackUsername(syncId: Int) = Preference.privateKey("pref_mangasync_username_$syncId")
private fun trackPassword(syncId: Int) =
Preference.privateKey("pref_mangasync_password_$syncId")
private fun trackToken(syncId: Int) = Preference.privateKey("track_token_$syncId")
}
}

View file

@ -5,7 +5,6 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.util.system.executeOnIO
@ -14,7 +13,7 @@ import uy.kohesive.injekt.injectLazy
abstract class TrackService(val id: Int) {
val preferences: PreferencesHelper by injectLazy()
val trackPreferences: TrackPreferences by injectLazy()
val networkService: NetworkHelper by injectLazy()
val db: DatabaseHelper by injectLazy()
open fun canRemoveFromService() = false
@ -93,19 +92,19 @@ abstract class TrackService(val id: Int) {
@CallSuper
open fun logout() {
preferences.setTrackCredentials(this, "", "")
trackPreferences.setCredentials(this, "", "")
}
open val isLogged: Boolean
get() = getUsername().isNotEmpty() &&
getPassword().isNotEmpty()
fun getUsername() = preferences.trackUsername(this)!!
fun getUsername() = trackPreferences.trackUsername(this).get()
fun getPassword() = preferences.trackPassword(this)!!
fun getPassword() = trackPreferences.trackPassword(this).get()
fun saveCredentials(username: String, password: String) {
preferences.setTrackCredentials(this, username, password)
trackPreferences.setCredentials(this, username, password)
}
}

View file

@ -41,7 +41,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
override val supportsReadingDates: Boolean = true
private val scorePreference = preferences.anilistScoreType()
private val scorePreference = trackPreferences.anilistScoreType()
init {
// If the preference is an int from APIv1, logout user to force using APIv2
@ -229,17 +229,17 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
override fun logout() {
super.logout()
preferences.trackToken(this).delete()
trackPreferences.trackToken(this).delete()
interceptor.setAuth(null)
}
fun saveOAuth(oAuth: OAuth?) {
preferences.trackToken(this).set(json.encodeToString(oAuth))
trackPreferences.trackToken(this).set(json.encodeToString(oAuth))
}
fun loadOAuth(): OAuth? {
return try {
json.decodeFromString<OAuth>(preferences.trackToken(this).get())
json.decodeFromString<OAuth>(trackPreferences.trackToken(this).get())
} catch (e: Exception) {
Timber.e(e)
null

View file

@ -129,12 +129,12 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
}
fun saveToken(oauth: OAuth?) {
preferences.trackToken(this).set(json.encodeToString(oauth))
trackPreferences.trackToken(this).set(json.encodeToString(oauth))
}
fun restoreToken(): OAuth? {
return try {
json.decodeFromString<OAuth>(preferences.trackToken(this).get())
json.decodeFromString<OAuth>(trackPreferences.trackToken(this).get())
} catch (e: Exception) {
null
}
@ -142,7 +142,7 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
override fun logout() {
super.logout()
preferences.trackToken(this).delete()
trackPreferences.trackToken(this).delete()
interceptor.newAuth(null)
}

View file

@ -154,12 +154,12 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
}
fun saveToken(oauth: OAuth?) {
preferences.trackToken(this).set(json.encodeToString(oauth))
trackPreferences.trackToken(this).set(json.encodeToString(oauth))
}
fun restoreToken(): OAuth? {
return try {
json.decodeFromString<OAuth>(preferences.trackToken(this).get())
json.decodeFromString<OAuth>(trackPreferences.trackToken(this).get())
} catch (e: Exception) {
null
}

View file

@ -130,6 +130,6 @@ class MangaUpdates(private val context: Context, id: Int) : TrackService(id) {
}
fun restoreSession(): String? {
return preferences.trackPassword(this)
return getPassword()
}
}

View file

@ -138,17 +138,17 @@ class MyAnimeList(private val context: Context, id: Int) : TrackService(id) {
override fun logout() {
super.logout()
preferences.trackToken(this).delete()
trackPreferences.trackToken(this).delete()
interceptor.setAuth(null)
}
fun saveOAuth(oAuth: OAuth?) {
preferences.trackToken(this).set(json.encodeToString(oAuth))
trackPreferences.trackToken(this).set(json.encodeToString(oAuth))
}
fun loadOAuth(): OAuth? {
return try {
json.decodeFromString<OAuth>(preferences.trackToken(this).get())
json.decodeFromString<OAuth>(trackPreferences.trackToken(this).get())
} catch (e: Exception) {
null
}

View file

@ -129,12 +129,12 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
}
fun saveToken(oauth: OAuth?) {
preferences.trackToken(this).set(json.encodeToString(oauth))
trackPreferences.trackToken(this).set(json.encodeToString(oauth))
}
fun restoreToken(): OAuth? {
return try {
json.decodeFromString<OAuth>(preferences.trackToken(this).get())
json.decodeFromString<OAuth>(trackPreferences.trackToken(this).get())
} catch (e: Exception) {
null
}
@ -142,7 +142,7 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
override fun logout() {
super.logout()
preferences.trackToken(this).delete()
trackPreferences.trackToken(this).delete()
interceptor.newAuth(null)
}
}

View file

@ -23,3 +23,6 @@ interface ConfigurableSource : Source {
// TODO: use getSourcePreferences once all extensions are on ext-lib 1.5
fun ConfigurableSource.sourcePreferences(): SharedPreferences =
Injekt.get<Application>().getSharedPreferences(preferenceKey(), Context.MODE_PRIVATE)
fun sourcePreferences(key: String): SharedPreferences =
Injekt.get<Application>().getSharedPreferences(key, Context.MODE_PRIVATE)

View file

@ -132,6 +132,8 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.Date
import java.util.concurrent.TimeUnit
@ -541,7 +543,7 @@ open class MainActivity : BaseActivity<MainActivityBinding>() {
preferences.incognitoMode().set(false)
// Show changelog if needed
if (Migrations.upgrade(preferences, lifecycleScope)) {
if (Migrations.upgrade(preferences, Injekt.get(), lifecycleScope)) {
if (!BuildConfig.DEBUG) {
content.post {
whatsNewSheet().show()

View file

@ -200,9 +200,7 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) :
return
}
if (track.tracking_url.isBlank()) {
activity.toast(R.string.url_not_set_click_again)
} else {
if (track.tracking_url.isNotBlank()) {
activity.openInBrowser(track.tracking_url.toUri())
controller.refreshTracker = position
}

View file

@ -218,6 +218,8 @@ class SettingsBackupController : SettingsController() {
R.string.chapters,
R.string.tracking,
R.string.history,
R.string.app_settings,
R.string.source_settings,
R.string.custom_manga_info,
R.string.all_read_manga,
)
@ -244,8 +246,10 @@ class SettingsBackupController : SettingsController() {
2 -> flags = flags or BackupConst.BACKUP_CHAPTER
3 -> flags = flags or BackupConst.BACKUP_TRACK
4 -> flags = flags or BackupConst.BACKUP_HISTORY
5 -> flags = flags or BackupConst.BACKUP_CUSTOM_INFO
6 -> flags = flags or BackupConst.BACKUP_READ_MANGA
5 -> flags = flags or BackupConst.BACKUP_APP_PREFS
6 -> flags = flags or BackupConst.BACKUP_SOURCE_PREFS
7 -> flags = flags or BackupConst.BACKUP_CUSTOM_INFO
8 -> flags = flags or BackupConst.BACKUP_READ_MANGA
}
}
}

View file

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.asImmediateFlowIn
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackPreferences
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
import eu.kanade.tachiyomi.data.track.bangumi.BangumiApi
@ -30,6 +31,7 @@ class SettingsTrackingController :
TrackLogoutDialog.Listener {
private val trackManager: TrackManager by injectLazy()
val trackPreferences: TrackPreferences by injectLazy()
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.tracking
@ -59,7 +61,7 @@ class SettingsTrackingController :
isIconSpaceReserved = true
title = context.getString(R.string.update_tracking_scoring_type, context.getString(R.string.anilist))
preferences.getStringPref(Keys.trackUsername(trackManager.aniList.id))
preferences.getStringPref(trackManager.aniList.getUsername())
.asImmediateFlowIn(viewScope) {
isVisible = it.isNotEmpty()
}
@ -117,7 +119,7 @@ class SettingsTrackingController :
): TrackerPreference {
return add(
TrackerPreference(context).apply {
key = Keys.trackUsername(service.id)
key = trackPreferences.trackUsername(service).key()
title = context.getString(service.nameRes())
iconRes = service.getLogo()
iconColor = service.getLogoColor()
@ -125,7 +127,7 @@ class SettingsTrackingController :
if (service.isLogged) {
if (service is EnhancedTrackService) {
service.logout()
updatePreference(service.id)
updatePreference(service)
} else {
val dialog = TrackLogoutDialog(service)
dialog.targetController = this@SettingsTrackingController
@ -134,7 +136,7 @@ class SettingsTrackingController :
} else {
if (service is EnhancedTrackService) {
service.loginNoop()
updatePreference(service.id)
updatePreference(service)
} else {
login()
}
@ -146,22 +148,22 @@ class SettingsTrackingController :
override fun onActivityResumed(activity: Activity) {
super.onActivityResumed(activity)
updatePreference(trackManager.myAnimeList.id)
updatePreference(trackManager.aniList.id)
updatePreference(trackManager.shikimori.id)
updatePreference(trackManager.bangumi.id)
updatePreference(trackManager.myAnimeList)
updatePreference(trackManager.aniList)
updatePreference(trackManager.shikimori)
updatePreference(trackManager.bangumi)
}
private fun updatePreference(id: Int) {
val pref = findPreference(Keys.trackUsername(id)) as? TrackerPreference
private fun updatePreference(service: TrackService) {
val pref = findPreference(trackPreferences.trackUsername(service).key()) as? TrackerPreference
pref?.notifyChanged()
}
override fun trackLoginDialogClosed(service: TrackService) {
updatePreference(service.id)
updatePreference(service)
}
override fun trackLogoutDialogClosed(service: TrackService) {
updatePreference(service.id)
updatePreference(service)
}
}

View file

@ -576,6 +576,8 @@
<string name="error_sharing_cover">Error sharing cover</string>
<string name="custom_manga_info">Custom manga info</string>
<string name="all_read_manga">All read manga</string>
<string name="app_settings">App settings</string>
<string name="source_settings">Source settings</string>
<string name="set_as_default">Set as default</string>
<string name="filter_groups">Filter scanlator groups</string>
<plurals name="deleted_chapters">