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 c62c6c12a..b6a866f3d 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 @@ -1,6 +1,5 @@ package eu.kanade.presentation.more.settings.screen -import android.content.ActivityNotFoundException import android.content.Context import android.content.Intent import android.net.Uri @@ -13,11 +12,9 @@ import androidx.annotation.StringRes import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -27,41 +24,34 @@ import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.runtime.toMutableStateList -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow import eu.kanade.presentation.more.settings.Preference +import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.permissions.PermissionRequestHelper import eu.kanade.presentation.util.relativeTimeSpanString import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.backup.BackupConst import eu.kanade.tachiyomi.data.backup.BackupCreateJob import eu.kanade.tachiyomi.data.backup.BackupFileValidator import eu.kanade.tachiyomi.data.backup.BackupRestoreJob -import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.copyToClipboard import eu.kanade.tachiyomi.util.system.toast -import kotlinx.coroutines.launch import logcat.LogPriority import tachiyomi.core.util.lang.launchNonCancellable import tachiyomi.core.util.lang.withUIContext import tachiyomi.core.util.system.logcat import tachiyomi.domain.backup.service.BackupPreferences import tachiyomi.domain.library.service.LibraryPreferences -import tachiyomi.presentation.core.components.LabeledCheckbox -import tachiyomi.presentation.core.components.ScrollbarLazyColumn import tachiyomi.presentation.core.util.collectAsState -import tachiyomi.presentation.core.util.isScrolledToEnd -import tachiyomi.presentation.core.util.isScrolledToStart import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -131,124 +121,11 @@ object SettingsDataScreen : SearchableSettings { @Composable private fun getCreateBackupPref(): Preference.PreferenceItem.TextPreference { - val scope = rememberCoroutineScope() - val context = LocalContext.current - - var flag by rememberSaveable { mutableIntStateOf(0) } - val chooseBackupDir = rememberLauncherForActivityResult( - contract = ActivityResultContracts.CreateDocument("application/*"), - ) { - if (it != null) { - context.contentResolver.takePersistableUriPermission( - it, - Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION, - ) - BackupCreateJob.startNow(context, it, flag) - } - flag = 0 - } - var showCreateDialog by rememberSaveable { mutableStateOf(false) } - if (showCreateDialog) { - CreateBackupDialog( - onConfirm = { - showCreateDialog = false - flag = it - try { - chooseBackupDir.launch(Backup.getFilename()) - } catch (e: ActivityNotFoundException) { - flag = 0 - context.toast(R.string.file_picker_error) - } - }, - onDismissRequest = { showCreateDialog = false }, - ) - } - + val navigator = LocalNavigator.currentOrThrow return Preference.PreferenceItem.TextPreference( title = stringResource(R.string.pref_create_backup), subtitle = stringResource(R.string.pref_create_backup_summ), - onClick = { - scope.launch { - if (!BackupCreateJob.isManualJobRunning(context)) { - if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { - context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG) - } - showCreateDialog = true - } else { - context.toast(R.string.backup_in_progress) - } - } - }, - ) - } - - @Composable - private fun CreateBackupDialog( - onConfirm: (flag: Int) -> Unit, - onDismissRequest: () -> Unit, - ) { - val choices = remember { - mapOf( - BackupConst.BACKUP_CATEGORY to R.string.categories, - BackupConst.BACKUP_CHAPTER to R.string.chapters, - BackupConst.BACKUP_TRACK to R.string.track, - BackupConst.BACKUP_HISTORY to R.string.history, - BackupConst.BACKUP_APP_PREFS to R.string.app_settings, - BackupConst.BACKUP_SOURCE_PREFS to R.string.source_settings, - ) - } - val flags = remember { choices.keys.toMutableStateList() } - AlertDialog( - onDismissRequest = onDismissRequest, - title = { Text(text = stringResource(R.string.backup_choice)) }, - text = { - Box { - val state = rememberLazyListState() - ScrollbarLazyColumn(state = state) { - item { - LabeledCheckbox( - label = stringResource(R.string.manga), - checked = true, - onCheckedChange = {}, - ) - } - choices.forEach { (k, v) -> - item { - val isSelected = flags.contains(k) - LabeledCheckbox( - label = stringResource(v), - checked = isSelected, - onCheckedChange = { - if (it) { - flags.add(k) - } else { - flags.remove(k) - } - }, - ) - } - } - } - if (!state.isScrolledToStart()) HorizontalDivider(modifier = Modifier.align(Alignment.TopCenter)) - if (!state.isScrolledToEnd()) HorizontalDivider(modifier = Modifier.align(Alignment.BottomCenter)) - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(R.string.action_cancel)) - } - }, - confirmButton = { - TextButton( - onClick = { - val flag = flags.fold(initial = 0, operation = { a, b -> a or b }) - onConfirm(flag) - }, - ) { - Text(text = stringResource(R.string.action_ok)) - } - }, + onClick = { navigator.push(CreateBackupScreen()) }, ) } @@ -336,7 +213,7 @@ object SettingsDataScreen : SearchableSettings { }, ) { if (it == null) { - error = InvalidRestore(message = context.getString(R.string.file_null_uri_error)) + context.toast(R.string.file_null_uri_error) return@rememberLauncherForActivityResult } 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 new file mode 100644 index 000000000..571a7bda8 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt @@ -0,0 +1,168 @@ +package eu.kanade.presentation.more.settings.screen.data + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Button +import androidx.compose.material3.HorizontalDivider +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.res.stringResource +import androidx.compose.ui.unit.dp +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.util.Screen +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.backup.BackupCreateFlags +import eu.kanade.tachiyomi.data.backup.BackupCreateJob +import eu.kanade.tachiyomi.data.backup.models.Backup +import eu.kanade.tachiyomi.util.system.DeviceUtil +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.update +import tachiyomi.presentation.core.components.LabeledCheckbox +import tachiyomi.presentation.core.components.material.Scaffold +import tachiyomi.presentation.core.components.material.padding + +class CreateBackupScreen : Screen() { + + @Composable + override fun Content() { + val context = LocalContext.current + val navigator = LocalNavigator.currentOrThrow + val model = rememberScreenModel { CreateBackupScreenModel() } + val state by model.state.collectAsState() + + val chooseBackupDir = rememberLauncherForActivityResult( + contract = ActivityResultContracts.CreateDocument("application/*"), + ) { + if (it != null) { + context.contentResolver.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION, + ) + model.createBackup(context, it) + navigator.pop() + } + } + + Scaffold( + topBar = { + AppBar( + title = stringResource(R.string.pref_create_backup), + navigateUp = navigator::pop, + scrollBehavior = it, + ) + }, + ) { contentPadding -> + Column( + modifier = Modifier + .padding(contentPadding) + .fillMaxSize(), + ) { + LazyColumn( + modifier = Modifier + .weight(1f) + .padding(horizontal = MaterialTheme.padding.medium), + ) { + item { + LabeledCheckbox( + label = stringResource(R.string.manga), + checked = true, + onCheckedChange = {}, + enabled = false, + ) + } + BackupChoices.forEach { (k, v) -> + item { + LabeledCheckbox( + label = stringResource(v), + checked = state.flags.contains(k), + onCheckedChange = { + model.toggleFlag(k) + }, + ) + } + } + } + + HorizontalDivider() + + Button( + modifier = Modifier + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + onClick = { + if (!BackupCreateJob.isManualJobRunning(context)) { + if (DeviceUtil.isMiui && DeviceUtil.isMiuiOptimizationDisabled()) { + context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG) + } + try { + chooseBackupDir.launch(Backup.getFilename()) + } catch (e: ActivityNotFoundException) { + context.toast(R.string.file_picker_error) + } + } else { + context.toast(R.string.backup_in_progress) + } + }, + ) { + Text( + text = stringResource(R.string.action_create), + color = MaterialTheme.colorScheme.onPrimary, + ) + } + } + } + } +} + +private class CreateBackupScreenModel : StateScreenModel(State()) { + + fun toggleFlag(flag: Int) { + mutableState.update { + if (it.flags.contains(flag)) { + it.copy(flags = it.flags - flag) + } else { + it.copy(flags = it.flags + flag) + } + } + } + + fun createBackup(context: Context, uri: Uri) { + val flags = state.value.flags.fold(initial = 0, operation = { a, b -> a or b }) + BackupCreateJob.startNow(context, uri, flags) + } + + @Immutable + data class State( + val flags: Set = BackupChoices.keys, + ) +} + +private val BackupChoices = mapOf( + BackupCreateFlags.BACKUP_CATEGORY to R.string.categories, + BackupCreateFlags.BACKUP_CHAPTER to R.string.chapters, + BackupCreateFlags.BACKUP_TRACK to R.string.track, + BackupCreateFlags.BACKUP_HISTORY to R.string.history, + BackupCreateFlags.BACKUP_APP_PREFS to R.string.app_settings, + BackupCreateFlags.BACKUP_SOURCE_PREFS to R.string.source_settings, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt deleted file mode 100644 index 6bc4771dc..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt +++ /dev/null @@ -1,24 +0,0 @@ -package eu.kanade.tachiyomi.data.backup - -// Filter options -internal object BackupConst { - const val BACKUP_CATEGORY = 0x1 - const val BACKUP_CATEGORY_MASK = 0x1 - - const val BACKUP_CHAPTER = 0x2 - const val BACKUP_CHAPTER_MASK = 0x2 - - const val BACKUP_HISTORY = 0x4 - const val BACKUP_HISTORY_MASK = 0x4 - - const val BACKUP_TRACK = 0x8 - const val BACKUP_TRACK_MASK = 0x8 - - const val BACKUP_APP_PREFS = 0x10 - const val BACKUP_APP_PREFS_MASK = 0x10 - - const val BACKUP_SOURCE_PREFS = 0x20 - const val BACKUP_SOURCE_PREFS_MASK = 0x20 - - const val BACKUP_ALL = 0x3F -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt new file mode 100644 index 000000000..7ae6edfde --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt @@ -0,0 +1,17 @@ +package eu.kanade.tachiyomi.data.backup + +internal object BackupCreateFlags { + const val BACKUP_CATEGORY = 0x1 + const val BACKUP_CHAPTER = 0x2 + const val BACKUP_HISTORY = 0x4 + const val BACKUP_TRACK = 0x8 + const val BACKUP_APP_PREFS = 0x10 + const val BACKUP_SOURCE_PREFS = 0x20 + + const val AutomaticDefaults = BACKUP_CATEGORY or + BACKUP_CHAPTER or + BACKUP_HISTORY or + BACKUP_TRACK or + BACKUP_APP_PREFS or + BACKUP_SOURCE_PREFS +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt index d46a58e00..4226e77fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt @@ -41,7 +41,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete val backupPreferences = Injekt.get() val uri = inputData.getString(LOCATION_URI_KEY)?.toUri() ?: backupPreferences.backupsDirectory().get().toUri() - val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL) + val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupCreateFlags.AutomaticDefaults) try { setForeground(getForegroundInfo()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt index 402bd0d94..a843a3b0c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt @@ -5,18 +5,12 @@ import android.content.Context import android.net.Uri import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS_MASK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK -import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK +import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_APP_PREFS +import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CATEGORY +import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CHAPTER +import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_HISTORY +import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_SOURCE_PREFS +import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_TRACK import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.BackupCategory import eu.kanade.tachiyomi.data.backup.models.BackupChapter @@ -161,7 +155,7 @@ class BackupCreator( */ private suspend fun backupCategories(options: Int): List { // Check if user wants category information in backup - return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { + return if (options and BACKUP_CATEGORY == BACKUP_CATEGORY) { getCategories.await() .filterNot(Category::isSystemCategory) .map(backupCategoryMapper) @@ -188,7 +182,7 @@ class BackupCreator( val mangaObject = BackupManga.copyFrom(manga) // Check if user wants chapter information in backup - if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) { + if (options and BACKUP_CHAPTER == BACKUP_CHAPTER) { // Backup all the chapters handler.awaitList { chaptersQueries.getChaptersByMangaId( @@ -202,7 +196,7 @@ class BackupCreator( } // Check if user wants category information in backup - if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { + if (options and BACKUP_CATEGORY == BACKUP_CATEGORY) { // Backup categories for this manga val categoriesForManga = getCategories.await(manga.id) if (categoriesForManga.isNotEmpty()) { @@ -211,7 +205,7 @@ class BackupCreator( } // Check if user wants track information in backup - if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) { + if (options and BACKUP_TRACK == BACKUP_TRACK) { val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) } if (tracks.isNotEmpty()) { mangaObject.tracking = tracks @@ -219,7 +213,7 @@ class BackupCreator( } // Check if user wants history information in backup - if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) { + if (options and BACKUP_HISTORY == BACKUP_HISTORY) { val historyByMangaId = getHistory.await(manga.id) if (historyByMangaId.isNotEmpty()) { val history = historyByMangaId.map { history -> @@ -236,13 +230,13 @@ class BackupCreator( } private fun backupAppPreferences(flags: Int): List { - if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList() + if (flags and BACKUP_APP_PREFS != BACKUP_APP_PREFS) return emptyList() return preferenceStore.getAll().toBackupPreferences() } private fun backupSourcePreferences(flags: Int): List { - if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList() + if (flags and BACKUP_SOURCE_PREFS != BACKUP_SOURCE_PREFS) return emptyList() return sourceManager.getCatalogueSources() .filterIsInstance() diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 0a9c92a55..47b7a9efa 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -484,6 +484,7 @@ Backup location Automatic backup frequency Maximum automatic backups + Create Backup created Invalid backup file Backup does not contain any library entries. @@ -880,7 +881,7 @@ Select cover image Select backup file No file picker app found - File picker failed to return file to app + No file selected Download 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 0a72e4b0b..f2039cd36 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 @@ -21,6 +21,7 @@ fun LabeledCheckbox( label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit, + enabled: Boolean = true, ) { Row( modifier = modifier @@ -37,6 +38,7 @@ fun LabeledCheckbox( Checkbox( checked = checked, onCheckedChange = null, + enabled = enabled, ) Text(text = label)