Browse Source

Convert create backup dialog to a screen

Allows us more flexibility in adding more options/explanations in the future.
arkon 1 year ago
parent
commit
00b2853d3d

+ 6 - 129
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
             }
 

+ 168 - 0
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<CreateBackupScreenModel.State>(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<Int> = 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,
+)

+ 0 - 24
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupConst.kt

@@ -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
-}

+ 17 - 0
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
+}

+ 1 - 1
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<BackupPreferences>()
         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())

+ 13 - 19
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<BackupCategory> {
         // 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<BackupPreference> {
-        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<BackupSourcePreferences> {
-        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<ConfigurableSource>()

+ 2 - 1
i18n/src/main/res/values/strings.xml

@@ -484,6 +484,7 @@
     <string name="pref_backup_directory">Backup location</string>
     <string name="pref_backup_interval">Automatic backup frequency</string>
     <string name="pref_backup_slots">Maximum automatic backups</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_missing_manga">Backup does not contain any library entries.</string>
@@ -880,7 +881,7 @@
     <string name="file_select_cover">Select cover image</string>
     <string name="file_select_backup">Select backup file</string>
     <string name="file_picker_error">No file picker app found</string>
-    <string name="file_null_uri_error">File picker failed to return file to app</string>
+    <string name="file_null_uri_error">No file selected</string>
 
     <!--UpdateCheck-->
     <string name="update_check_confirm">Download</string>

+ 2 - 0
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)