parent
32c3269291
commit
5bba7af24a
9 changed files with 286 additions and 161 deletions
|
@ -1,6 +1,7 @@
|
||||||
package eu.kanade.presentation.more.settings.screen
|
package eu.kanade.presentation.more.settings.screen
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException
|
import android.content.ActivityNotFoundException
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||||
|
@ -33,7 +34,9 @@ import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
||||||
import eu.kanade.presentation.util.relativeTimeSpanString
|
import eu.kanade.presentation.util.relativeTimeSpanString
|
||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||||
|
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
@ -139,6 +142,22 @@ object SettingsDataScreen : SearchableSettings {
|
||||||
|
|
||||||
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
|
val lastAutoBackup by backupPreferences.lastAutoBackupTimestamp().collectAsState()
|
||||||
|
|
||||||
|
val chooseBackup = rememberLauncherForActivityResult(
|
||||||
|
object : ActivityResultContracts.GetContent() {
|
||||||
|
override fun createIntent(context: Context, input: String): Intent {
|
||||||
|
val intent = super.createIntent(context, input)
|
||||||
|
return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
if (it == null) {
|
||||||
|
context.toast(MR.strings.file_null_uri_error)
|
||||||
|
return@rememberLauncherForActivityResult
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.push(RestoreBackupScreen(it))
|
||||||
|
}
|
||||||
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.label_backup),
|
title = stringResource(MR.strings.label_backup),
|
||||||
preferenceItems = persistentListOf(
|
preferenceItems = persistentListOf(
|
||||||
|
@ -162,7 +181,18 @@ object SettingsDataScreen : SearchableSettings {
|
||||||
}
|
}
|
||||||
SegmentedButton(
|
SegmentedButton(
|
||||||
checked = false,
|
checked = false,
|
||||||
onCheckedChange = { navigator.push(RestoreBackupScreen()) },
|
onCheckedChange = {
|
||||||
|
if (!BackupRestoreJob.isRunning(context)) {
|
||||||
|
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||||
|
context.toast(MR.strings.restore_miui_warning)
|
||||||
|
}
|
||||||
|
|
||||||
|
// no need to catch because it's wrapped with a chooser
|
||||||
|
chooseBackup.launch("*/*")
|
||||||
|
} else {
|
||||||
|
context.toast(MR.strings.restore_in_progress)
|
||||||
|
}
|
||||||
|
},
|
||||||
shape = SegmentedButtonDefaults.itemShape(1, 2),
|
shape = SegmentedButtonDefaults.itemShape(1, 2),
|
||||||
) {
|
) {
|
||||||
Text(stringResource(MR.strings.pref_restore_backup))
|
Text(stringResource(MR.strings.pref_restore_backup))
|
||||||
|
|
|
@ -1,28 +1,26 @@
|
||||||
package eu.kanade.presentation.more.settings.screen.data
|
package eu.kanade.presentation.more.settings.screen.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
@ -34,22 +32,23 @@ import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
|
import eu.kanade.tachiyomi.data.backup.restore.RestoreOptions
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import tachiyomi.core.i18n.stringResource
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.LabeledCheckbox
|
||||||
|
import tachiyomi.presentation.core.components.SectionCard
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
|
|
||||||
class RestoreBackupScreen : Screen() {
|
class RestoreBackupScreen(
|
||||||
|
private val uri: Uri,
|
||||||
|
) : Screen() {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val model = rememberScreenModel { RestoreBackupScreenModel() }
|
val model = rememberScreenModel { RestoreBackupScreenModel(context, uri) }
|
||||||
val state by model.state.collectAsState()
|
val state by model.state.collectAsState()
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
@ -61,171 +60,181 @@ class RestoreBackupScreen : Screen() {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
if (state.error != null) {
|
Column(
|
||||||
val onDismissRequest = model::clearError
|
|
||||||
when (val err = state.error) {
|
|
||||||
is InvalidRestore -> {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
title = { Text(text = stringResource(MR.strings.invalid_backup_file)) },
|
|
||||||
text = { Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n")) },
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
context.copyToClipboard(err.message, err.message)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(MR.strings.action_copy_to_clipboard))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onDismissRequest) {
|
|
||||||
Text(text = stringResource(MR.strings.action_ok))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
is MissingRestoreComponents -> {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
title = { Text(text = stringResource(MR.strings.pref_restore_backup)) },
|
|
||||||
text = {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
|
||||||
) {
|
|
||||||
val msg = buildString {
|
|
||||||
append(stringResource(MR.strings.backup_restore_content_full))
|
|
||||||
if (err.sources.isNotEmpty()) {
|
|
||||||
append(
|
|
||||||
"\n\n",
|
|
||||||
).append(stringResource(MR.strings.backup_restore_missing_sources))
|
|
||||||
err.sources.joinTo(
|
|
||||||
this,
|
|
||||||
separator = "\n- ",
|
|
||||||
prefix = "\n- ",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (err.trackers.isNotEmpty()) {
|
|
||||||
append(
|
|
||||||
"\n\n",
|
|
||||||
).append(stringResource(MR.strings.backup_restore_missing_trackers))
|
|
||||||
err.trackers.joinTo(
|
|
||||||
this,
|
|
||||||
separator = "\n- ",
|
|
||||||
prefix = "\n- ",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Text(text = msg)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
BackupRestoreJob.start(
|
|
||||||
context = context,
|
|
||||||
uri = err.uri,
|
|
||||||
options = state.options,
|
|
||||||
)
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Text(text = stringResource(MR.strings.action_restore))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> onDismissRequest() // Unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val chooseBackup = rememberLauncherForActivityResult(
|
|
||||||
object : ActivityResultContracts.GetContent() {
|
|
||||||
override fun createIntent(context: Context, input: String): Intent {
|
|
||||||
val intent = super.createIntent(context, input)
|
|
||||||
return Intent.createChooser(intent, context.stringResource(MR.strings.file_select_backup))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
if (it == null) {
|
|
||||||
context.toast(MR.strings.file_null_uri_error)
|
|
||||||
return@rememberLauncherForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
val results = try {
|
|
||||||
BackupFileValidator(context).validate(it)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
model.setError(InvalidRestore(it, e.message.toString()))
|
|
||||||
return@rememberLauncherForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
if (results.missingSources.isEmpty() && results.missingTrackers.isEmpty()) {
|
|
||||||
BackupRestoreJob.start(
|
|
||||||
context = context,
|
|
||||||
uri = it,
|
|
||||||
options = state.options,
|
|
||||||
)
|
|
||||||
return@rememberLauncherForActivityResult
|
|
||||||
}
|
|
||||||
|
|
||||||
model.setError(MissingRestoreComponents(it, results.missingSources, results.missingTrackers))
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(contentPadding)
|
.padding(contentPadding)
|
||||||
.fillMaxSize(),
|
.fillMaxSize(),
|
||||||
) {
|
) {
|
||||||
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
LazyColumn(
|
||||||
item {
|
modifier = Modifier.weight(1f),
|
||||||
WarningBanner(MR.strings.restore_miui_warning)
|
) {
|
||||||
|
if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) {
|
||||||
|
item {
|
||||||
|
WarningBanner(MR.strings.restore_miui_warning)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
if (state.canRestore) {
|
||||||
Button(
|
item {
|
||||||
modifier = Modifier
|
SectionCard {
|
||||||
.padding(horizontal = MaterialTheme.padding.medium)
|
RestoreOptions.options.forEach { option ->
|
||||||
.fillMaxWidth(),
|
LabeledCheckbox(
|
||||||
onClick = {
|
label = stringResource(option.label),
|
||||||
if (!BackupRestoreJob.isRunning(context)) {
|
checked = option.getter(state.options),
|
||||||
// no need to catch because it's wrapped with a chooser
|
onCheckedChange = {
|
||||||
chooseBackup.launch("*/*")
|
model.toggle(option.setter, it)
|
||||||
} else {
|
},
|
||||||
context.toast(MR.strings.restore_in_progress)
|
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
) {
|
}
|
||||||
Text(stringResource(MR.strings.pref_restore_backup))
|
|
||||||
|
if (state.error != null) {
|
||||||
|
errorMessageItem(state, model)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: show validation errors inline
|
HorizontalDivider()
|
||||||
// TODO: show options for what to restore
|
|
||||||
|
Button(
|
||||||
|
enabled = state.canRestore && state.options.anyEnabled(),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
onClick = {
|
||||||
|
model.startRestore()
|
||||||
|
navigator.pop()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(MR.strings.action_restore),
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyListScope.errorMessageItem(
|
||||||
|
state: RestoreBackupScreenModel.State,
|
||||||
|
model: RestoreBackupScreenModel,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SectionCard {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||||
|
) {
|
||||||
|
when (val err = state.error) {
|
||||||
|
is MissingRestoreComponents -> {
|
||||||
|
val msg = buildString {
|
||||||
|
append(stringResource(MR.strings.backup_restore_content_full))
|
||||||
|
if (err.sources.isNotEmpty()) {
|
||||||
|
append("\n\n")
|
||||||
|
append(stringResource(MR.strings.backup_restore_missing_sources))
|
||||||
|
err.sources.joinTo(
|
||||||
|
this,
|
||||||
|
separator = "\n- ",
|
||||||
|
prefix = "\n- ",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (err.trackers.isNotEmpty()) {
|
||||||
|
append("\n\n")
|
||||||
|
append(stringResource(MR.strings.backup_restore_missing_trackers))
|
||||||
|
err.trackers.joinTo(
|
||||||
|
this,
|
||||||
|
separator = "\n- ",
|
||||||
|
prefix = "\n- ",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SelectionContainer {
|
||||||
|
Text(text = msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is InvalidRestore -> {
|
||||||
|
Text(text = stringResource(MR.strings.invalid_backup_file))
|
||||||
|
|
||||||
|
SelectionContainer {
|
||||||
|
Text(text = listOfNotNull(err.uri, err.message).joinToString("\n\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
SelectionContainer {
|
||||||
|
Text(text = err.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class RestoreBackupScreenModel : StateScreenModel<RestoreBackupScreenModel.State>(State()) {
|
private class RestoreBackupScreenModel(
|
||||||
|
private val context: Context,
|
||||||
|
private val uri: Uri,
|
||||||
|
) : StateScreenModel<RestoreBackupScreenModel.State>(State()) {
|
||||||
|
|
||||||
fun setError(error: Any) {
|
init {
|
||||||
|
validate(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validate(uri: Uri) {
|
||||||
|
val results = try {
|
||||||
|
BackupFileValidator(context).validate(uri)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
setError(
|
||||||
|
error = InvalidRestore(uri, e.message.toString()),
|
||||||
|
canRestore = false,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.missingSources.isNotEmpty() || results.missingTrackers.isNotEmpty()) {
|
||||||
|
setError(
|
||||||
|
error = MissingRestoreComponents(uri, results.missingSources, results.missingTrackers),
|
||||||
|
canRestore = true,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(error = null, canRestore = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) {
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
it.copy(error = error)
|
it.copy(
|
||||||
|
options = setter(it.options, enabled),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearError() {
|
fun startRestore() {
|
||||||
|
BackupRestoreJob.start(
|
||||||
|
context = context,
|
||||||
|
uri = uri,
|
||||||
|
options = state.value.options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setError(error: Any?, canRestore: Boolean) {
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
it.copy(error = null)
|
it.copy(
|
||||||
|
error = error,
|
||||||
|
canRestore = canRestore,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class State(
|
data class State(
|
||||||
val error: Any? = null,
|
val error: Any? = null,
|
||||||
// TODO: allow user-selectable restore options
|
val canRestore: Boolean = false,
|
||||||
val options: RestoreOptions = RestoreOptions(),
|
val options: RestoreOptions = RestoreOptions(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,13 +19,13 @@ import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
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.cancelNotification
|
||||||
import eu.kanade.tachiyomi.util.system.isRunning
|
import eu.kanade.tachiyomi.util.system.isRunning
|
||||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||||
import eu.kanade.tachiyomi.util.system.workManager
|
import eu.kanade.tachiyomi.util.system.workManager
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
|
import tachiyomi.core.util.lang.asBooleanArray
|
||||||
|
import tachiyomi.core.util.lang.asDataClass
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.domain.backup.service.BackupPreferences
|
import tachiyomi.domain.backup.service.BackupPreferences
|
||||||
import tachiyomi.domain.storage.service.StorageManager
|
import tachiyomi.domain.storage.service.StorageManager
|
||||||
|
|
|
@ -13,8 +13,6 @@ import androidx.work.WorkerParameters
|
||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
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.cancelNotification
|
||||||
import eu.kanade.tachiyomi.util.system.isRunning
|
import eu.kanade.tachiyomi.util.system.isRunning
|
||||||
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
import eu.kanade.tachiyomi.util.system.setForegroundSafely
|
||||||
|
@ -22,6 +20,8 @@ import eu.kanade.tachiyomi.util.system.workManager
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.i18n.stringResource
|
import tachiyomi.core.i18n.stringResource
|
||||||
|
import tachiyomi.core.util.lang.asBooleanArray
|
||||||
|
import tachiyomi.core.util.lang.asDataClass
|
||||||
import tachiyomi.core.util.system.logcat
|
import tachiyomi.core.util.system.logcat
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,40 @@
|
||||||
package eu.kanade.tachiyomi.data.backup.restore
|
package eu.kanade.tachiyomi.data.backup.restore
|
||||||
|
|
||||||
|
import dev.icerock.moko.resources.StringResource
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import tachiyomi.i18n.MR
|
||||||
|
|
||||||
data class RestoreOptions(
|
data class RestoreOptions(
|
||||||
|
val library: Boolean = true,
|
||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
val sourceSettings: Boolean = true,
|
val sourceSettings: Boolean = true,
|
||||||
val library: Boolean = true,
|
) {
|
||||||
)
|
|
||||||
|
fun anyEnabled() = library || appSettings || sourceSettings
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val options = persistentListOf(
|
||||||
|
Entry(
|
||||||
|
label = MR.strings.label_library,
|
||||||
|
getter = RestoreOptions::library,
|
||||||
|
setter = { options, enabled -> options.copy(library = enabled) },
|
||||||
|
),
|
||||||
|
Entry(
|
||||||
|
label = MR.strings.app_settings,
|
||||||
|
getter = RestoreOptions::appSettings,
|
||||||
|
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
||||||
|
),
|
||||||
|
Entry(
|
||||||
|
label = MR.strings.source_settings,
|
||||||
|
getter = RestoreOptions::sourceSettings,
|
||||||
|
setter = { options, enabled -> options.copy(sourceSettings = enabled) },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Entry(
|
||||||
|
val label: StringResource,
|
||||||
|
val getter: (RestoreOptions) -> Boolean,
|
||||||
|
val setter: (RestoreOptions, Boolean) -> RestoreOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ dependencies {
|
||||||
|
|
||||||
implementation(libs.unifile)
|
implementation(libs.unifile)
|
||||||
|
|
||||||
|
implementation(kotlinx.reflect)
|
||||||
api(kotlinx.coroutines.core)
|
api(kotlinx.coroutines.core)
|
||||||
api(kotlinx.serialization.json)
|
api(kotlinx.serialization.json)
|
||||||
api(kotlinx.serialization.json.okio)
|
api(kotlinx.serialization.json.okio)
|
||||||
|
@ -46,4 +47,6 @@ dependencies {
|
||||||
|
|
||||||
// JavaScript engine
|
// JavaScript engine
|
||||||
implementation(libs.bundles.js.engine)
|
implementation(libs.bundles.js.engine)
|
||||||
|
|
||||||
|
testImplementation(libs.bundles.test)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
package eu.kanade.tachiyomi.util.lang
|
package tachiyomi.core.util.lang
|
||||||
|
|
||||||
import kotlin.reflect.KProperty1
|
import kotlin.reflect.KProperty1
|
||||||
import kotlin.reflect.full.declaredMemberProperties
|
import kotlin.reflect.full.declaredMemberProperties
|
||||||
import kotlin.reflect.full.primaryConstructor
|
import kotlin.reflect.full.primaryConstructor
|
||||||
|
|
||||||
fun <T : Any> T.asBooleanArray(): BooleanArray {
|
fun <T : Any> T.asBooleanArray(): BooleanArray {
|
||||||
return this::class.declaredMemberProperties
|
val constructorParams = this::class.primaryConstructor!!.parameters.map { it.name }
|
||||||
|
val properties = this::class.declaredMemberProperties
|
||||||
.filterIsInstance<KProperty1<T, Boolean>>()
|
.filterIsInstance<KProperty1<T, Boolean>>()
|
||||||
.map { it.get(this) }
|
return constructorParams
|
||||||
|
.map { param -> properties.find { it.name == param }!!.get(this) }
|
||||||
.toBooleanArray()
|
.toBooleanArray()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
package tachiyomi.core.util.lang
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.assertArrayEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
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 {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `converts 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`() {
|
||||||
|
assertThrows<ClassCastException> {
|
||||||
|
InvalidTestClass(foo = true, bar = "").asBooleanArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `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`() {
|
||||||
|
assertThrows<IllegalArgumentException> {
|
||||||
|
booleanArrayOf(true).asDataClass<TestClass>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TestClass(
|
||||||
|
val foo: Boolean,
|
||||||
|
val bar: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class InvalidTestClass(
|
||||||
|
val foo: Boolean,
|
||||||
|
val bar: String,
|
||||||
|
)
|
||||||
|
}
|
|
@ -497,7 +497,7 @@
|
||||||
<string name="invalid_backup_file_missing_manga">Backup does not contain any library entries.</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_sources">Missing sources:</string>
|
||||||
<string name="backup_restore_missing_trackers">Trackers not logged into:</string>
|
<string name="backup_restore_missing_trackers">Trackers not logged into:</string>
|
||||||
<string name="backup_restore_content_full">Data from the backup file will be restored.\n\nYou will need to install any missing extensions and log in to tracking services afterwards to use them.</string>
|
<string name="backup_restore_content_full">Data from the backup file will be restored.\n\nYou may need to install any missing extensions and log in to tracking services afterwards to use them.</string>
|
||||||
<string name="restore_completed">Restore completed</string>
|
<string name="restore_completed">Restore completed</string>
|
||||||
<string name="restore_duration">%02d min, %02d sec</string>
|
<string name="restore_duration">%02d min, %02d sec</string>
|
||||||
<string name="backup_in_progress">Backup is already in progress</string>
|
<string name="backup_in_progress">Backup is already in progress</string>
|
||||||
|
|
Reference in a new issue