Add ability to create manual backups with private preferences too

This commit is contained in:
arkon 2023-12-28 17:38:37 -05:00
parent 8735836498
commit ccec5c3efe
9 changed files with 65 additions and 77 deletions

View file

@ -154,14 +154,6 @@ private class CreateBackupScreenModel : StateScreenModel<CreateBackupScreenModel
@Immutable
data class State(
val options: BackupOptions = BackupOptions(
libraryEntries = true,
categories = true,
chapters = true,
tracking = true,
history = true,
appSettings = false,
sourceSettings = false,
),
val options: BackupOptions = BackupOptions(),
)
}

View file

@ -19,6 +19,8 @@ import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.lang.asBooleanArray
import eu.kanade.tachiyomi.util.lang.asDataClass
import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely
@ -47,9 +49,8 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
setForegroundSafely()
val options = inputData.getBooleanArray(OPTIONS_KEY)
?.let { BackupOptions.fromBooleanArray(it) }
?: BackupOptions.AutomaticDefaults
val options: BackupOptions = inputData.getBooleanArray(OPTIONS_KEY)?.asDataClass()
?: BackupOptions()
return try {
val location = BackupCreator(context, isAutoBackup).backup(uri, options)
@ -118,7 +119,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
val inputData = workDataOf(
IS_AUTO_BACKUP_KEY to false,
LOCATION_URI_KEY to uri.toString(),
OPTIONS_KEY to options.toBooleanArray(),
OPTIONS_KEY to options.asBooleanArray(),
)
val request = OneTimeWorkRequestBuilder<BackupCreateJob>()
.addTag(TAG_MANUAL)

View file

@ -131,13 +131,13 @@ class BackupCreator(
private fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
if (!options.appSettings) return emptyList()
return preferenceBackupCreator.backupAppPreferences()
return preferenceBackupCreator.backupAppPreferences(includePrivatePreferences = options.privateSettings)
}
private fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
if (!options.sourceSettings) return emptyList()
return preferenceBackupCreator.backupSourcePreferences()
return preferenceBackupCreator.backupSourcePreferences(includePrivatePreferences = options.privateSettings)
}
companion object {

View file

@ -12,75 +12,52 @@ data class BackupOptions(
val history: Boolean = true,
val appSettings: Boolean = true,
val sourceSettings: Boolean = true,
val privateSettings: Boolean = false,
) {
fun toBooleanArray() = booleanArrayOf(
libraryEntries,
categories,
chapters,
tracking,
history,
appSettings,
sourceSettings,
)
companion object {
val AutomaticDefaults = BackupOptions(
libraryEntries = true,
categories = true,
chapters = true,
tracking = true,
history = true,
appSettings = true,
sourceSettings = true,
)
fun fromBooleanArray(booleanArray: BooleanArray) = BackupOptions(
libraryEntries = booleanArray[0],
categories = booleanArray[1],
chapters = booleanArray[2],
tracking = booleanArray[3],
history = booleanArray[4],
appSettings = booleanArray[5],
sourceSettings = booleanArray[6],
)
val entries = persistentListOf<BackupOptionEntry>(
BackupOptionEntry(
val entries = persistentListOf(
Entry(
label = MR.strings.categories,
getter = BackupOptions::categories,
setter = { options, enabled -> options.copy(categories = enabled) },
),
BackupOptionEntry(
Entry(
label = MR.strings.chapters,
getter = BackupOptions::chapters,
setter = { options, enabled -> options.copy(chapters = enabled) },
),
BackupOptionEntry(
Entry(
label = MR.strings.track,
getter = BackupOptions::tracking,
setter = { options, enabled -> options.copy(tracking = enabled) },
),
BackupOptionEntry(
Entry(
label = MR.strings.history,
getter = BackupOptions::history,
setter = { options, enabled -> options.copy(history = enabled) },
),
BackupOptionEntry(
Entry(
label = MR.strings.app_settings,
getter = BackupOptions::appSettings,
setter = { options, enabled -> options.copy(appSettings = enabled) },
),
BackupOptionEntry(
Entry(
label = MR.strings.source_settings,
getter = BackupOptions::sourceSettings,
setter = { options, enabled -> options.copy(sourceSettings = enabled) },
),
Entry(
label = MR.strings.private_settings,
getter = BackupOptions::privateSettings,
setter = { options, enabled -> options.copy(privateSettings = enabled) },
),
)
}
}
data class BackupOptionEntry(
data class Entry(
val label: StringResource,
val getter: (BackupOptions) -> Boolean,
val setter: (BackupOptions, Boolean) -> BackupOptions,
)
}

View file

@ -22,26 +22,27 @@ class PreferenceBackupCreator(
private val preferenceStore: PreferenceStore = Injekt.get(),
) {
fun backupAppPreferences(): List<BackupPreference> {
fun backupAppPreferences(includePrivatePreferences: Boolean): List<BackupPreference> {
return preferenceStore.getAll().toBackupPreferences()
.withPrivatePreferences(includePrivatePreferences)
}
fun backupSourcePreferences(): List<BackupSourcePreferences> {
fun backupSourcePreferences(includePrivatePreferences: Boolean): List<BackupSourcePreferences> {
return sourceManager.getCatalogueSources()
.filterIsInstance<ConfigurableSource>()
.map {
BackupSourcePreferences(
it.preferenceKey(),
it.sourcePreferences().all.toBackupPreferences(),
it.sourcePreferences().all.toBackupPreferences()
.withPrivatePreferences(includePrivatePreferences),
)
}
}
@Suppress("UNCHECKED_CAST")
private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
return this.filterKeys {
!Preference.isAppState(it) && !Preference.isPrivate(it)
}
return this
.filterKeys { !Preference.isAppState(it) }
.mapNotNull { (key, value) ->
when (value) {
is Int -> BackupPreference(key, IntPreferenceValue(value))
@ -56,4 +57,11 @@ class PreferenceBackupCreator(
}
}
}
private fun List<BackupPreference>.withPrivatePreferences(include: Boolean) =
if (include) {
this
} else {
this.filter { !Preference.isPrivate(it.key) }
}
}

View file

@ -13,6 +13,8 @@ import androidx.work.WorkerParameters
import androidx.work.workDataOf
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.lang.asBooleanArray
import eu.kanade.tachiyomi.util.lang.asDataClass
import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.isRunning
import eu.kanade.tachiyomi.util.system.setForegroundSafely
@ -30,8 +32,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
override suspend fun doWork(): Result {
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
val options = inputData.getBooleanArray(OPTIONS_KEY)
?.let { RestoreOptions.fromBooleanArray(it) }
val options: RestoreOptions? = inputData.getBooleanArray(OPTIONS_KEY)?.asDataClass()
if (uri == null || options == null) {
return Result.failure()
@ -84,7 +85,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
val inputData = workDataOf(
LOCATION_URI_KEY to uri.toString(),
SYNC_KEY to sync,
OPTIONS_KEY to options.toBooleanArray(),
OPTIONS_KEY to options.asBooleanArray(),
)
val request = OneTimeWorkRequestBuilder<BackupRestoreJob>()
.addTag(TAG)

View file

@ -4,14 +4,4 @@ data class RestoreOptions(
val appSettings: Boolean = true,
val sourceSettings: Boolean = true,
val library: Boolean = true,
) {
fun toBooleanArray() = booleanArrayOf(appSettings, sourceSettings, library)
companion object {
fun fromBooleanArray(booleanArray: BooleanArray) = RestoreOptions(
appSettings = booleanArray[0],
sourceSettings = booleanArray[1],
library = booleanArray[2],
)
}
}

View file

@ -0,0 +1,18 @@
package eu.kanade.tachiyomi.util.lang
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.primaryConstructor
fun <T : Any> T.asBooleanArray(): BooleanArray {
return this::class.declaredMemberProperties
.filterIsInstance<KProperty1<T, Boolean>>()
.map { it.get(this) }
.toBooleanArray()
}
inline fun <reified T : Any> BooleanArray.asDataClass(): T {
val properties = T::class.declaredMemberProperties.filterIsInstance<KProperty1<T, Boolean>>()
require(properties.size == this.size) { "Boolean array size does not match data class property count" }
return T::class.primaryConstructor!!.call(*this.toTypedArray())
}

View file

@ -504,6 +504,7 @@
<string name="backup_choice">What do you want to backup?</string>
<string name="app_settings">App settings</string>
<string name="source_settings">Source settings</string>
<string name="private_settings">Include sensitive settings (e.g., tracker login tokens)</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>