Skip to content

Commit

Permalink
Allow creating backups without library entries
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
arkon committed Dec 30, 2023
1 parent f3b7eaf commit f0a0ecf
Show file tree
Hide file tree
Showing 10 changed files with 89 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ object SettingsDataScreen : SearchableSettings {
return@rememberLauncherForActivityResult
}

navigator.push(RestoreBackupScreen(it))
navigator.push(RestoreBackupScreen(it.toString()))
}

return Preference.PreferenceGroup(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -126,48 +132,57 @@ 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 -> {
Text(text = stringResource(MR.strings.invalid_backup_file))
is InvalidRestore -> {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
appendLine(stringResource(MR.strings.invalid_backup_file))
}
appendLine(error.uri.toString())

appendLine()

SelectionContainer {
Text(text = listOfNotNull(error.uri, error.message).joinToString("\n\n"))
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
appendLine(stringResource(MR.strings.invalid_backup_file_error))
}
appendLine(error.message)
}
}

else -> {
SelectionContainer {
Text(text = error.toString())
else -> {
appendLine(error.toString())
}
}
}

SelectionContainer {
Text(text = msg)
}
}
}
}
Expand All @@ -176,11 +191,11 @@ class RestoreBackupScreen(

private class RestoreBackupScreenModel(
private val context: Context,
private val uri: Uri,
private val uri: String,
) : StateScreenModel<RestoreBackupScreenModel.State>(State()) {

init {
validate(uri)
validate(uri.toUri())
}

fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) {
Expand All @@ -194,7 +209,7 @@ private class RestoreBackupScreenModel(
fun startRestore() {
BackupRestoreJob.start(
context = context,
uri = uri,
uri = uri.toUri(),
options = state.value.options,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 {
Expand All @@ -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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
),
)

Expand All @@ -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 },
),
)
}
Expand All @@ -62,5 +72,6 @@ data class BackupOptions(
val label: StringResource,
val getter: (BackupOptions) -> Boolean,
val setter: (BackupOptions, Boolean) -> BackupOptions,
val enabled: (BackupOptions) -> Boolean = { true },
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ data class RestoreOptions(
val sourceSettings: Boolean = true,
) {

fun anyEnabled() = library || appSettings || sourceSettings

companion object {
val options = persistentListOf(
Entry(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,9 @@ inline fun <reified T : Any> 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 : Any> T.anyEnabled(): Boolean {
return this::class.declaredMemberProperties
.filterIsInstance<KProperty1<T, Boolean>>()
.any { it.get(this) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClassCastException> {
InvalidTestClass(foo = true, bar = "").asBooleanArray()
}
}

@Test
fun `converts from boolean array`() {
fun `asDataClass converts from boolean array`() {
assertEquals(booleanArrayOf(true, false).asDataClass<TestClass>(), TestClass(foo = true, bar = false))
assertEquals(booleanArrayOf(false, true).asDataClass<TestClass>(), TestClass(foo = false, bar = true))
}

@Test
fun `throws error for invalid boolean array`() {
fun `asDataClass throws error for invalid boolean array`() {
assertThrows<IllegalArgumentException> {
booleanArrayOf(true).asDataClass<TestClass>()
}
}

@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<ClassCastException> {
InvalidTestClass(foo = true, bar = "").anyEnabled()
}
}

data class TestClass(
val foo: Boolean,
val bar: Boolean,
Expand Down
3 changes: 2 additions & 1 deletion i18n/src/commonMain/resources/MR/base/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,8 @@
<string name="pref_backup_interval">Automatic backup frequency</string>
<string name="action_create">Create</string>
<string name="backup_created">Backup created</string>
<string name="invalid_backup_file">Invalid backup file</string>
<string name="invalid_backup_file">Invalid backup file:</string>
<string name="invalid_backup_file_error">Full error:</string>
<string name="invalid_backup_file_missing_manga">Backup does not contain any library entries.</string>
<string name="backup_restore_missing_sources">Missing sources:</string>
<string name="backup_restore_missing_trackers">Trackers not logged into:</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit f0a0ecf

Please sign in to comment.