package eu.kanade.presentation.more.settings.screen.data import android.content.Context import android.net.Uri import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable 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.core.net.toUri import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.components.AppBar import eu.kanade.presentation.components.WarningBanner import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.data.backup.BackupFileValidator 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.i18n.MR import tachiyomi.presentation.core.components.LabeledCheckbox import tachiyomi.presentation.core.components.LazyColumnWithAction import tachiyomi.presentation.core.components.SectionCard import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.padding import tachiyomi.presentation.core.i18n.stringResource class RestoreBackupScreen( private val uri: String, ) : Screen() { @Composable override fun Content() { val context = LocalContext.current val navigator = LocalNavigator.currentOrThrow val model = rememberScreenModel { RestoreBackupScreenModel(context, uri) } val state by model.state.collectAsState() Scaffold( topBar = { AppBar( title = stringResource(MR.strings.pref_restore_backup), navigateUp = navigator::pop, scrollBehavior = it, ) }, ) { contentPadding -> LazyColumnWithAction( contentPadding = contentPadding, actionLabel = stringResource(MR.strings.action_restore), actionEnabled = state.canRestore && state.options.anyEnabled(), onClickAction = { model.startRestore() navigator.pop() }, ) { if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { item { WarningBanner(MR.strings.restore_miui_warning) } } if (state.canRestore) { item { SectionCard { RestoreOptions.options.forEach { option -> LabeledCheckbox( label = stringResource(option.label), checked = option.getter(state.options), onCheckedChange = { model.toggle(option.setter, it) }, ) } } } } if (state.error != null) { errorMessageItem(state.error) } } } } private fun LazyListScope.errorMessageItem( error: Any?, ) { item { SectionCard { Column( modifier = Modifier.padding(horizontal = MaterialTheme.padding.medium), verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), ) { val msg = buildAnnotatedString { when (error) { is MissingRestoreComponents -> { appendLine(stringResource(MR.strings.backup_restore_content_full)) if (error.sources.isNotEmpty()) { appendLine() withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { appendLine(stringResource(MR.strings.backup_restore_missing_sources)) } error.sources.joinTo( this, separator = "\n- ", prefix = "- ", ) } if (error.trackers.isNotEmpty()) { appendLine() withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { appendLine(stringResource(MR.strings.backup_restore_missing_trackers)) } error.trackers.joinTo( this, separator = "\n- ", prefix = "- ", ) } } 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()) } } } SelectionContainer { Text(text = msg) } } } } } } private class RestoreBackupScreenModel( private val context: Context, private val uri: String, ) : StateScreenModel(State()) { init { validate(uri.toUri()) } fun toggle(setter: (RestoreOptions, Boolean) -> RestoreOptions, enabled: Boolean) { mutableState.update { it.copy( options = setter(it.options, enabled), ) } } fun startRestore() { BackupRestoreJob.start( context = context, uri = uri.toUri(), options = state.value.options, ) } 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) } private fun setError(error: Any?, canRestore: Boolean) { mutableState.update { it.copy( error = error, canRestore = canRestore, ) } } @Immutable data class State( val error: Any? = null, val canRestore: Boolean = false, val options: RestoreOptions = RestoreOptions(), ) } private data class MissingRestoreComponents( val uri: Uri, val sources: List, val trackers: List, ) private data class InvalidRestore( val uri: Uri? = null, val message: String, )