Add app settings to backups

This should be compatible with Aniyomi's implementation.
Related to #1857

Co-authored-by: jmir1 <jmir1@users.noreply.github.com>
This commit is contained in:
arkon 2023-10-08 10:40:58 -04:00
parent 9c688b08c0
commit 72024aa44a
11 changed files with 141 additions and 9 deletions

View file

@ -147,6 +147,7 @@ object SettingsBackupScreen : SearchableSettings {
BackupConst.BACKUP_CHAPTER to R.string.chapters,
BackupConst.BACKUP_TRACK to R.string.track,
BackupConst.BACKUP_HISTORY to R.string.history,
BackupConst.BACKUP_APP_PREFS to R.string.app_settings,
)
}
val flags = remember { choices.keys.toMutableStateList() }

View file

@ -4,11 +4,18 @@ package eu.kanade.tachiyomi.data.backup
internal object BackupConst {
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_ALL = 0xF
const val BACKUP_APP_PREFS = 0x10
const val BACKUP_APP_PREFS_MASK = 0x10
const val BACKUP_ALL = 0x1F
}

View file

@ -6,6 +6,8 @@ import android.net.Uri
import com.hippo.unifile.UniFile
import eu.kanade.domain.chapter.model.copyFrom
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
@ -18,8 +20,15 @@ import eu.kanade.tachiyomi.data.backup.models.Backup
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.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.backup.models.backupCategoryMapper
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
@ -30,6 +39,7 @@ import logcat.LogPriority
import okio.buffer
import okio.gzip
import okio.sink
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.core.util.system.logcat
import tachiyomi.data.DatabaseHandler
import tachiyomi.data.Manga_sync
@ -61,6 +71,7 @@ class BackupManager(
private val getCategories: GetCategories = Injekt.get()
private val getFavorites: GetFavorites = Injekt.get()
private val getHistory: GetHistory = Injekt.get()
private val preferenceStore: PreferenceStore = Injekt.get()
internal val parser = ProtoBuf
@ -81,6 +92,7 @@ class BackupManager(
backupCategories(flags),
emptyList(),
prepExtensionInfoForSync(databaseManga),
backupAppPreferences(flags),
)
var file: UniFile? = null
@ -133,7 +145,7 @@ class BackupManager(
}
}
fun prepExtensionInfoForSync(mangas: List<Manga>): List<BackupSource> {
private fun prepExtensionInfoForSync(mangas: List<Manga>): List<BackupSource> {
return mangas
.asSequence()
.map(Manga::source)
@ -148,7 +160,7 @@ class BackupManager(
*
* @return list of [BackupCategory] to be backed up
*/
suspend fun backupCategories(options: Int): List<BackupCategory> {
private suspend fun backupCategories(options: Int): List<BackupCategory> {
// Check if user wants category information in backup
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
getCategories.await()
@ -159,7 +171,7 @@ class BackupManager(
}
}
suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
private suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
return mangas.map {
backupManga(it, flags)
}
@ -219,6 +231,25 @@ class BackupManager(
return mangaObject
}
@Suppress("UNCHECKED_CAST")
private fun backupAppPreferences(flags: Int): List<BackupPreference> {
if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList()
return preferenceStore.getAll().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
}
}
}
internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
var updatedManga = manga.copy(id = dbManga._id)
updatedManga = updatedManga.copyFrom(dbManga)

View file

@ -7,13 +7,20 @@ 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.BackupSource
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.util.BackupUtil
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
import tachiyomi.core.preference.PreferenceStore
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.repository.ChapterRepository
import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.model.Track
@ -30,8 +37,8 @@ class BackupRestorer(
private val notifier: BackupNotifier,
) {
private val updateManga: UpdateManga = Injekt.get()
private val chapterRepository: ChapterRepository = Injekt.get()
private val fetchInterval: FetchInterval = Injekt.get()
private val preferenceStore: PreferenceStore = Injekt.get()
private var now = ZonedDateTime.now()
private var currentFetchWindow = fetchInterval.getWindow(now)
@ -106,6 +113,8 @@ class BackupRestorer(
currentFetchWindow = fetchInterval.getWindow(now)
return coroutineScope {
restoreAppPreferences(backup.backupPreferences)
// Restore individual manga
backup.backupManga.forEach {
if (!isActive) {
@ -115,6 +124,7 @@ class BackupRestorer(
restoreManga(it, backup.backupCategories, sync)
}
// TODO: optionally trigger online library + tracker update
true
}
}
@ -200,6 +210,45 @@ class BackupRestorer(
backupManager.restoreTracking(manga, tracks)
}
private fun restoreAppPreferences(preferences: List<BackupPreference>) {
val prefs = preferenceStore.getAll()
preferences.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)
}
}
}
}
}
/**
* Called to update dialog in [BackupConst]
*

View file

@ -11,9 +11,9 @@ 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(),
) {
companion object {

View file

@ -9,7 +9,6 @@ class BackupCategory(
@ProtoNumber(1) var name: String,
@ProtoNumber(2) var order: Long = 0,
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
// Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var flags: Long = 0,
) {
fun getCategory(): Category {

View file

@ -0,0 +1,31 @@
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
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

@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import tachiyomi.core.util.system.logcat
sealed class AndroidPreference<T>(
private val preferences: SharedPreferences,
@ -29,7 +30,13 @@ sealed class AndroidPreference<T>(
}
override fun get(): T {
return read(preferences, key, defaultValue)
return try {
read(preferences, key, defaultValue)
} catch (e: ClassCastException) {
logcat { "Invalid value for $key; deleting" }
delete()
defaultValue
}
}
override fun set(value: T) {

View file

@ -60,6 +60,10 @@ class AndroidPreferenceStore(
deserializer = deserializer,
)
}
override fun getAll(): Map<String, *> {
return sharedPreferences.all ?: emptyMap<String, Any>()
}
}
private val SharedPreferences.keyFlow

View file

@ -20,6 +20,8 @@ interface PreferenceStore {
serializer: (T) -> String,
deserializer: (String) -> T,
): Preference<T>
fun getAll(): Map<String, *>
}
inline fun <reified T : Enum<T>> PreferenceStore.getEnum(

View file

@ -493,6 +493,7 @@
</plurals>
<string name="backup_in_progress">Backup is already in progress</string>
<string name="backup_choice">What do you want to backup?</string>
<string name="app_settings">App settings</string>
<string name="creating_backup">Creating backup</string>
<string name="creating_backup_error">Backup failed</string>
<string name="missing_storage_permission">Storage permissions not granted</string>