From f0a0ecfd4a5c8ee85fdcf7e92dc9a0079ef40cde Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 30 Dec 2023 16:02:36 -0500 Subject: [PATCH] Allow creating backups without library entries - In case you want a backup of just settings? - Also disable backup options if dependent option is disabled (and fix being able to toggle disabled items) - Also fix crash in RestoreBackupScreen due to attempt to parcelize Uri - Make restore validation message a bit nicer --- .../settings/screen/SettingsDataScreen.kt | 2 +- .../screen/data/CreateBackupScreen.kt | 13 +--- .../screen/data/RestoreBackupScreen.kt | 75 +++++++++++-------- .../data/backup/BackupFileValidator.kt | 7 -- .../data/backup/create/BackupOptions.kt | 11 +++ .../data/backup/restore/RestoreOptions.kt | 2 - ...sions.kt => BooleanDataClassExtensions.kt} | 6 ++ ...t.kt => BooleanDataClassExtensionsTest.kt} | 25 +++++-- .../commonMain/resources/MR/base/strings.xml | 3 +- .../core/components/LabeledCheckbox.kt | 6 +- 10 files changed, 92 insertions(+), 58 deletions(-) rename core/src/main/java/tachiyomi/core/util/lang/{BooleanArrayExtensions.kt => BooleanDataClassExtensions.kt} (83%) rename core/src/test/kotlin/tachiyomi/core/util/lang/{BooleanArrayExtensionsTest.kt => BooleanDataClassExtensionsTest.kt} (61%) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index 964ea202b..fd2491ea9 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -155,7 +155,7 @@ object SettingsDataScreen : SearchableSettings { return@rememberLauncherForActivityResult } - navigator.push(RestoreBackupScreen(it)) + navigator.push(RestoreBackupScreen(it.toString())) } return Preference.PreferenceGroup( diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt index 0ac316e24..4e221a7b5 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt @@ -92,17 +92,7 @@ class CreateBackupScreen : Screen() { item { SectionCard(MR.strings.label_library) { - Column { - LabeledCheckbox( - label = stringResource(MR.strings.manga), - checked = true, - onCheckedChange = {}, - enabled = false, - modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), - ) - - Options(BackupOptions.libraryOptions, state, model) - } + Options(BackupOptions.libraryOptions, state, model) } } @@ -153,6 +143,7 @@ class CreateBackupScreen : Screen() { onCheckedChange = { model.toggle(option.setter, it) }, + enabled = option.enabled(state.options), modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), ) } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt index 13cae0962..43a1b3650 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/RestoreBackupScreen.kt @@ -20,7 +20,12 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator @@ -33,6 +38,7 @@ import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions import eu.kanade.tachiyomi.util.system.DeviceUtil import kotlinx.coroutines.flow.update +import tachiyomi.core.util.lang.anyEnabled import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.SectionCard @@ -41,7 +47,7 @@ import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource class RestoreBackupScreen( - private val uri: Uri, + private val uri: String, ) : Screen() { @Composable @@ -99,10 +105,10 @@ class RestoreBackupScreen( HorizontalDivider() Button( - enabled = state.canRestore && state.options.anyEnabled(), modifier = Modifier .padding(horizontal = 16.dp, vertical = 8.dp) .fillMaxWidth(), + enabled = state.canRestore && state.options.anyEnabled(), onClick = { model.startRestore() navigator.pop() @@ -126,47 +132,56 @@ class RestoreBackupScreen( modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { - when (error) { - is MissingRestoreComponents -> { - val msg = buildString { - append(stringResource(MR.strings.backup_restore_content_full)) + val msg = buildAnnotatedString { + when (error) { + is MissingRestoreComponents -> { + appendLine(stringResource(MR.strings.backup_restore_content_full)) if (error.sources.isNotEmpty()) { - append("\n\n") - append(stringResource(MR.strings.backup_restore_missing_sources)) + appendLine() + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + appendLine(stringResource(MR.strings.backup_restore_missing_sources)) + } error.sources.joinTo( this, separator = "\n- ", - prefix = "\n- ", + prefix = "- ", ) } if (error.trackers.isNotEmpty()) { - append("\n\n") - append(stringResource(MR.strings.backup_restore_missing_trackers)) + appendLine() + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + appendLine(stringResource(MR.strings.backup_restore_missing_trackers)) + } error.trackers.joinTo( this, separator = "\n- ", - prefix = "\n- ", + prefix = "- ", ) } } - SelectionContainer { - Text(text = msg) + + is InvalidRestore -> { + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + appendLine(stringResource(MR.strings.invalid_backup_file)) + } + appendLine(error.uri.toString()) + + appendLine() + + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + appendLine(stringResource(MR.strings.invalid_backup_file_error)) + } + appendLine(error.message) + } + + else -> { + appendLine(error.toString()) } } + } - is InvalidRestore -> { - Text(text = stringResource(MR.strings.invalid_backup_file)) - - SelectionContainer { - Text(text = listOfNotNull(error.uri, error.message).joinToString("\n\n")) - } - } - - else -> { - SelectionContainer { - Text(text = error.toString()) - } - } + SelectionContainer { + Text(text = msg) } } } @@ -176,11 +191,11 @@ class RestoreBackupScreen( private class RestoreBackupScreenModel( private val context: Context, - private val uri: Uri, + private val uri: String, ) : StateScreenModel(State()) { init { - validate(uri) + validate(uri.toUri()) } fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) { @@ -194,7 +209,7 @@ private class RestoreBackupScreenModel( fun startRestore() { BackupRestoreJob.start( context = context, - uri = uri, + uri = uri.toUri(), options = state.value.options, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt index acc768e5a..5a7b87ce9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupFileValidator.kt @@ -3,9 +3,7 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri import eu.kanade.tachiyomi.data.track.TrackerManager -import tachiyomi.core.i18n.stringResource import tachiyomi.domain.source.service.SourceManager -import tachiyomi.i18n.MR import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -19,7 +17,6 @@ class BackupFileValidator( /** * Checks for critical backup file data. * - * @throws Exception if manga cannot be found. * @return List of missing sources or missing trackers. */ fun validate(uri: Uri): Results { @@ -29,10 +26,6 @@ class BackupFileValidator( throw IllegalStateException(e) } - if (backup.backupManga.isEmpty()) { - throw IllegalStateException(context.stringResource(MR.strings.invalid_backup_file_missing_manga)) - } - val sources = backup.backupSources.associate { it.sourceId to it.name } val missingSources = sources .filter { sourceManager.get(it.key) == null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt index 14a75ee42..7fc4dff1c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupOptions.kt @@ -17,25 +17,34 @@ data class BackupOptions( companion object { val libraryOptions = persistentListOf( + Entry( + label = MR.strings.manga, + getter = BackupOptions::libraryEntries, + setter = { options, enabled -> options.copy(libraryEntries = enabled) }, + ), Entry( label = MR.strings.categories, getter = BackupOptions::categories, setter = { options, enabled -> options.copy(categories = enabled) }, + enabled = { it.libraryEntries }, ), Entry( label = MR.strings.chapters, getter = BackupOptions::chapters, setter = { options, enabled -> options.copy(chapters = enabled) }, + enabled = { it.libraryEntries }, ), Entry( label = MR.strings.track, getter = BackupOptions::tracking, setter = { options, enabled -> options.copy(tracking = enabled) }, + enabled = { it.libraryEntries }, ), Entry( label = MR.strings.history, getter = BackupOptions::history, setter = { options, enabled -> options.copy(history = enabled) }, + enabled = { it.libraryEntries }, ), ) @@ -54,6 +63,7 @@ data class BackupOptions( label = MR.strings.private_settings, getter = BackupOptions::privateSettings, setter = { options, enabled -> options.copy(privateSettings = enabled) }, + enabled = { it.appSettings || it.sourceSettings }, ), ) } @@ -62,5 +72,6 @@ data class BackupOptions( val label: StringResource, val getter: (BackupOptions) -> Boolean, val setter: (BackupOptions, Boolean) -> BackupOptions, + val enabled: (BackupOptions) -> Boolean = { true }, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt index 6905331fd..3f5a9290c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/RestoreOptions.kt @@ -10,8 +10,6 @@ data class RestoreOptions( val sourceSettings: Boolean = true, ) { - fun anyEnabled() = library || appSettings || sourceSettings - companion object { val options = persistentListOf( Entry( diff --git a/core/src/main/java/tachiyomi/core/util/lang/BooleanArrayExtensions.kt b/core/src/main/java/tachiyomi/core/util/lang/BooleanDataClassExtensions.kt similarity index 83% rename from core/src/main/java/tachiyomi/core/util/lang/BooleanArrayExtensions.kt rename to core/src/main/java/tachiyomi/core/util/lang/BooleanDataClassExtensions.kt index b06cf6161..d781c678f 100644 --- a/core/src/main/java/tachiyomi/core/util/lang/BooleanArrayExtensions.kt +++ b/core/src/main/java/tachiyomi/core/util/lang/BooleanDataClassExtensions.kt @@ -18,3 +18,9 @@ inline fun BooleanArray.asDataClass(): T { require(properties.size == this.size) { "Boolean array size does not match data class property count" } return T::class.primaryConstructor!!.call(*this.toTypedArray()) } + +fun T.anyEnabled(): Boolean { + return this::class.declaredMemberProperties + .filterIsInstance>() + .any { it.get(this) } +} diff --git a/core/src/test/kotlin/tachiyomi/core/util/lang/BooleanArrayExtensionsTest.kt b/core/src/test/kotlin/tachiyomi/core/util/lang/BooleanDataClassExtensionsTest.kt similarity index 61% rename from core/src/test/kotlin/tachiyomi/core/util/lang/BooleanArrayExtensionsTest.kt rename to core/src/test/kotlin/tachiyomi/core/util/lang/BooleanDataClassExtensionsTest.kt index 59bf479e1..d75e7b3f8 100644 --- a/core/src/test/kotlin/tachiyomi/core/util/lang/BooleanArrayExtensionsTest.kt +++ b/core/src/test/kotlin/tachiyomi/core/util/lang/BooleanDataClassExtensionsTest.kt @@ -2,40 +2,55 @@ package tachiyomi.core.util.lang import org.junit.jupiter.api.Assertions.assertArrayEquals import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.parallel.Execution import org.junit.jupiter.api.parallel.ExecutionMode @Execution(ExecutionMode.CONCURRENT) -class BooleanArrayExtensionsTest { +class BooleanDataClassExtensionsTest { @Test - fun `converts to boolean array`() { + fun `asBooleanArray converts data class to boolean array`() { assertArrayEquals(booleanArrayOf(true, false), TestClass(foo = true, bar = false).asBooleanArray()) assertArrayEquals(booleanArrayOf(false, true), TestClass(foo = false, bar = true).asBooleanArray()) } @Test - fun `throws error for invalid data classes`() { + fun `asBooleanArray throws error for invalid data classes`() { assertThrows { InvalidTestClass(foo = true, bar = "").asBooleanArray() } } @Test - fun `converts from boolean array`() { + fun `asDataClass converts from boolean array`() { assertEquals(booleanArrayOf(true, false).asDataClass(), TestClass(foo = true, bar = false)) assertEquals(booleanArrayOf(false, true).asDataClass(), TestClass(foo = false, bar = true)) } @Test - fun `throws error for invalid boolean array`() { + fun `asDataClass throws error for invalid boolean array`() { assertThrows { booleanArrayOf(true).asDataClass() } } + @Test + fun `anyEnabled returns based on if any boolean property is enabled`() { + assertTrue(TestClass(foo = false, bar = true).anyEnabled()) + assertFalse(TestClass(foo = false, bar = false).anyEnabled()) + } + + @Test + fun `anyEnabled throws error for invalid class`() { + assertThrows { + InvalidTestClass(foo = true, bar = "").anyEnabled() + } + } + data class TestClass( val foo: Boolean, val bar: Boolean, diff --git a/i18n/src/commonMain/resources/MR/base/strings.xml b/i18n/src/commonMain/resources/MR/base/strings.xml index b2c124e1d..0a91a88dd 100644 --- a/i18n/src/commonMain/resources/MR/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/base/strings.xml @@ -493,7 +493,8 @@ Automatic backup frequency Create Backup created - Invalid backup file + Invalid backup file: + Full error: Backup does not contain any library entries. Missing sources: Trackers not logged into: diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt index b4a7fb1d8..a66bf0d18 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/LabeledCheckbox.kt @@ -31,7 +31,11 @@ fun LabeledCheckbox( .heightIn(min = 48.dp) .clickable( role = Role.Checkbox, - onClick = { onCheckedChange(!checked) }, + onClick = { + if (enabled) { + onCheckedChange(!checked) + } + }, ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),