Просмотр исходного кода

备份可选网络、本地、网络与本地同时备份

HapeLee 6 месяцев назад
Родитель
Сommit
bc1d21f1f2

+ 20 - 20
app/src/main/java/io/legado/app/help/storage/Backup.kt

@@ -14,7 +14,6 @@ import io.legado.app.help.DirectLinkUpload
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.config.OldThemeConfig
-import io.legado.app.help.config.PersonalizationThemeConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.model.BookCover
@@ -84,7 +83,6 @@ object Backup {
             ReadBookConfig.configFileName,
             ReadBookConfig.shareConfigFileName,
             OldThemeConfig.configFileName,
-            PersonalizationThemeConfig.configFileName,
             BookCover.configFileName,
             "config.xml"
         )
@@ -125,15 +123,15 @@ object Backup {
         }
     }
 
-    suspend fun backupLocked(context: Context, path: String?) {
+    suspend fun backupLocked(context: Context, path: String?, mode: String = "both") {
         mutex.withLock {
             withContext(IO) {
-                backup(context, path)
+                backup(context, path, mode)
             }
         }
     }
 
-    private suspend fun backup(context: Context, path: String?) {
+    private suspend fun backup(context: Context, path: String?, mode: String = "both") {
         LogUtils.d(TAG, "开始备份 path:$path")
         LocalConfig.lastBackup = System.currentTimeMillis()
         val aes = BackupAES()
@@ -183,8 +181,6 @@ object Backup {
             FileUtils.createFileIfNotExist(backupPath + File.separator + BookCover.configFileName)
                 .writeText(GSON.toJson(it))
         }
-        FileUtils.createFileIfNotExist(backupPath + File.separator + PersonalizationThemeConfig.configFileName)
-            .writeText(PersonalizationThemeConfig.toJson())
         currentCoroutineContext().ensureActive()
         appCtx.getSharedPreferences(backupPath, "config")?.let { sp ->
             sp.edit(commit = true) {
@@ -223,23 +219,27 @@ object Backup {
             zipFileName
         }
         if (ZipUtils.zipFiles(paths, zipFilePath)) {
-            when {
-                path.isNullOrBlank() -> {
-                    copyBackup(context.getExternalFilesDir(null)!!, backupFileName)
-                }
+            if (mode == "both" || mode == "local") {
+                when {
+                    path.isNullOrBlank() -> {
+                        copyBackup(context.getExternalFilesDir(null)!!, backupFileName)
+                    }
 
-                path.isContentScheme() -> {
-                    copyBackup(context, path.toUri(), backupFileName)
-                }
+                    path.isContentScheme() -> {
+                        copyBackup(context, path.toUri(), backupFileName)
+                    }
 
-                else -> {
-                    copyBackup(File(path), backupFileName)
+                    else -> {
+                        copyBackup(File(path), backupFileName)
+                    }
                 }
             }
-            try {
-                AppWebDav.backUpWebDav(zipFileName)
-            } catch (e: Exception) {
-                AppLog.put("上传备份至webdav失败\n$e", e)
+            if (mode == "both" || mode == "webdav") {
+                try {
+                    AppWebDav.backUpWebDav(zipFileName)
+                } catch (e: Exception) {
+                    AppLog.put("上传备份至webdav失败\n$e", e)
+                }
             }
         }
         FileUtils.delete(backupPath)

+ 0 - 5
app/src/main/java/io/legado/app/help/storage/BackupConfig.kt

@@ -23,7 +23,6 @@ object BackupConfig {
     private const val readConfigKey = "readConfig"
     private const val themeConfigKey = "themeConfig"
     private const val coverConfigKey = "coverConfig"
-    private const val personalizationThemeConfigKey = "personalizationThemeConfig"
     private const val localBookKey = "localBook"
 
     //配置忽略key
@@ -31,7 +30,6 @@ object BackupConfig {
         readConfigKey,
         PreferKey.themeMode,
         themeConfigKey,
-        personalizationThemeConfigKey,
         coverConfigKey,
         PreferKey.bookshelfLayout,
         PreferKey.showRss,
@@ -44,7 +42,6 @@ object BackupConfig {
         appCtx.getString(R.string.read_config),
         appCtx.getString(R.string.theme_mode),
         appCtx.getString(R.string.theme_config),
-        "深度个性化主题",
         appCtx.getString(R.string.cover_config),
         appCtx.getString(R.string.bookshelf_layout),
         appCtx.getString(R.string.show_rss),
@@ -125,8 +122,6 @@ object BackupConfig {
         get() = ignoreConfig[themeConfigKey] == true
     private val ignoreCoverConfig: Boolean
         get() = ignoreConfig[coverConfigKey] == true
-    val ignorePersonalizationThemeConfig: Boolean
-        get() = ignoreConfig[personalizationThemeConfigKey] == true
     private val ignoreBookshelfLayout: Boolean
         get() = ignoreConfig[PreferKey.bookshelfLayout] == true
     private val ignoreShowRss: Boolean

+ 5 - 0
app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfig.kt

@@ -50,6 +50,11 @@ object BackupConfig {
         true
     )
 
+    var backupSyncMode by prefDelegate(
+        PreferKey.backupSyncMode,
+        "both"
+    )
+
     var backupPath by prefDelegate<String?>(
         PreferKey.backupPath,
         null

+ 149 - 82
app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigScreen.kt

@@ -14,6 +14,9 @@ import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Cloud
+import androidx.compose.material.icons.filled.Lan
+import androidx.compose.material.icons.filled.PhoneAndroid
 import androidx.compose.material.icons.filled.Visibility
 import androidx.compose.material.icons.filled.VisibilityOff
 import androidx.compose.material3.ExperimentalMaterial3Api
@@ -33,6 +36,7 @@ import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.input.nestedscroll.nestedScroll
 import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringArrayResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.text.input.PasswordVisualTransformation
@@ -54,7 +58,10 @@ import io.legado.app.ui.widget.components.card.SelectionItemCard
 import io.legado.app.ui.widget.components.checkBox.CheckboxItem
 import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
+import io.legado.app.ui.widget.components.modalBottomSheet.OptionCard
+import io.legado.app.ui.widget.components.modalBottomSheet.OptionSheet
 import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
+import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
 import io.legado.app.ui.widget.components.settingItem.InputSettingItem
 import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
@@ -90,6 +97,8 @@ fun BackupConfigScreen(
     var showBackupFilePicker by remember { mutableStateOf(false) }
     var showRestoreFilePicker by remember { mutableStateOf(false) }
     var showRestoreSheet by remember { mutableStateOf(false) }
+    var showRestoreOptionSheet by remember { mutableStateOf(false) }
+    var showBackupOptionSheet by remember { mutableStateOf(false) }
     var backupNames by remember { mutableStateOf<List<String>>(emptyList()) }
 
     var showLoadingDialog by remember { mutableStateOf(false) }
@@ -134,7 +143,7 @@ fun BackupConfigScreen(
             }
             BackupConfig.backupPath = path
             if (path.isNotEmpty()) {
-                startBackup(path, context, viewModel, {
+                startBackup(path, context, viewModel, "both", {
                     showLoadingDialog = false
                     scope.launch {
                         snackbarHostState.showSnackbar(context.getString(R.string.backup_success))
@@ -285,6 +294,15 @@ fun BackupConfigScreen(
                     checked = BackupConfig.autoCheckNewBackup,
                     onCheckedChange = { BackupConfig.autoCheckNewBackup = it }
                 )
+
+                DropdownListSettingItem(
+                    title = stringResource(R.string.backup_sync_mode),
+                    description = stringResource(R.string.backup_sync_mode_summary),
+                    selectedValue = BackupConfig.backupSyncMode,
+                    displayEntries = stringArrayResource(R.array.backup_sync_mode),
+                    entryValues = stringArrayResource(R.array.backup_sync_mode_value),
+                    onValueChange = { BackupConfig.backupSyncMode = it }
+                )
             }
 
                 SplicedColumnGroup(title = stringResource(R.string.backup_restore)) {
@@ -295,94 +313,16 @@ fun BackupConfigScreen(
                     onClick = { showBackupFilePicker = true }
                 )
 
-                val backupText = stringResource(R.string.backup)
-
                 ClickableSettingItem(
                     title = stringResource(R.string.backup),
                     description = stringResource(R.string.backup_summary),
-                    onClick = {
-                        val backupPath = BackupConfig.backupPath
-                        if (backupPath.isNullOrEmpty()) {
-                            showRestoreFilePicker = true
-                        } else {
-                            confirmDialogTitle = backupText
-                            confirmDialogText = "确定要备份吗?"
-                            onConfirmAction = {
-                                if (backupPath.isContentScheme()) {
-                                    startBackup(backupPath, context, viewModel, {
-                                        showLoadingDialog = false
-                                        scope.launch {
-                                            snackbarHostState.showSnackbar(context.getString(R.string.backup_success))
-                                        }
-                                    }, { error ->
-                                        showLoadingDialog = false
-                                        scope.launch {
-                                            snackbarHostState.showSnackbar(
-                                                context.getString(
-                                                    R.string.backup_fail,
-                                                    error
-                                                )
-                                            )
-                                        }
-                                    })
-                                } else {
-                                    PermissionsCompat.Builder()
-                                        .addPermissions(*Permissions.Group.STORAGE)
-                                        .rationale(R.string.tip_perm_request_storage)
-                                        .onGranted {
-                                            startBackup(backupPath, context, viewModel, {
-                                                showLoadingDialog = false
-                                                scope.launch {
-                                                    snackbarHostState.showSnackbar(
-                                                        context.getString(
-                                                            R.string.backup_success
-                                                        )
-                                                    )
-                                                }
-                                            }, { error ->
-                                                showLoadingDialog = false
-                                                scope.launch {
-                                                    snackbarHostState.showSnackbar(
-                                                        context.getString(
-                                                            R.string.backup_fail,
-                                                            error
-                                                        )
-                                                    )
-                                                }
-                                            })
-                                        }
-                                        .request()
-                                }
-                            }
-                            showConfirmDialog = true
-                        }
-                    }
+                    onClick = { showBackupOptionSheet = true }
                 )
 
                 ClickableSettingItem(
                     title = stringResource(R.string.restore),
                     description = stringResource(R.string.restore_summary),
-                    onClick = {
-                        scope.launch {
-                            showLoadingDialog = true
-                            loadingText = "加载中"
-                            try {
-                                val names = viewModel.getBackupNames()
-                                backupNames = names
-                                showRestoreSheet = true
-                            } catch (e: Exception) {
-                                confirmDialogTitle = "恢复"
-                                confirmDialogText =
-                                    "WebDavError\n${e.localizedMessage}\n将从本地备份恢复。"
-                                onConfirmAction = {
-                                    restoreFileLauncher.launch(arrayOf("application/zip"))
-                                }
-                                showConfirmDialog = true
-                            } finally {
-                                showLoadingDialog = false
-                            }
-                        }
-                    }
+                    onClick = { showRestoreOptionSheet = true }
                 )
 
                 ClickableSettingItem(
@@ -499,6 +439,84 @@ fun BackupConfigScreen(
         }
     )
 
+    OptionSheet(
+        show = showBackupOptionSheet,
+        onDismissRequest = { showBackupOptionSheet = false },
+        title = stringResource(R.string.backup)
+    ) {
+        OptionCard(
+            icon = Icons.Default.PhoneAndroid,
+            text = stringResource(R.string.backup_to_local),
+            onClick = {
+                showBackupOptionSheet = false
+                executeBackup("local", context, viewModel, scope, snackbarHostState, {
+                    showLoadingDialog = false
+                }, { showLoadingDialog = true; loadingText = it })
+            }
+        )
+        OptionCard(
+            icon = Icons.Default.Cloud,
+            text = stringResource(R.string.backup_to_network),
+            onClick = {
+                showBackupOptionSheet = false
+                executeBackup("webdav", context, viewModel, scope, snackbarHostState, {
+                    showLoadingDialog = false
+                }, { showLoadingDialog = true; loadingText = it })
+            }
+        )
+        OptionCard(
+            icon = Icons.Default.Lan,
+            text = stringResource(R.string.backup_to_local_and_network),
+            onClick = {
+                showBackupOptionSheet = false
+                executeBackup("both", context, viewModel, scope, snackbarHostState, {
+                    showLoadingDialog = false
+                }, { showLoadingDialog = true; loadingText = it })
+            }
+        )
+    }
+
+    OptionSheet(
+        show = showRestoreOptionSheet,
+        onDismissRequest = { showRestoreOptionSheet = false },
+        title = stringResource(R.string.restore)
+    ) {
+        OptionCard(
+            icon = Icons.Default.PhoneAndroid,
+            text = "本地恢复",
+            onClick = {
+                showRestoreOptionSheet = false
+                restoreFileLauncher.launch(arrayOf("application/zip"))
+            }
+        )
+        OptionCard(
+            icon = Icons.Default.Cloud,
+            text = "网络恢复",
+            onClick = {
+                showRestoreOptionSheet = false
+                scope.launch {
+                    showLoadingDialog = true
+                    loadingText = "加载中"
+                    try {
+                        val names = viewModel.getBackupNames()
+                        backupNames = names
+                        showRestoreSheet = true
+                    } catch (e: Exception) {
+                        confirmDialogTitle = "恢复"
+                        confirmDialogText =
+                            "WebDavError\n${e.localizedMessage}\n将从本地备份恢复。"
+                        onConfirmAction = {
+                            restoreFileLauncher.launch(arrayOf("application/zip"))
+                        }
+                        showConfirmDialog = true
+                    } finally {
+                        showLoadingDialog = false
+                    }
+                }
+            }
+        )
+    }
+
     AppModalBottomSheet(
         show = showRestoreSheet && backupNames.isNotEmpty(),
         onDismissRequest = { showRestoreSheet = false },
@@ -621,8 +639,57 @@ private fun startBackup(
     path: String,
     context: Context,
     viewModel: BackupConfigViewModel,
+    mode: String = "both",
     onSuccess: () -> Unit,
     onError: (String) -> Unit
 ) {
-    viewModel.backup(path, onSuccess, onError)
+    viewModel.backup(path, mode, onSuccess, onError)
+}
+
+private fun executeBackup(
+    mode: String,
+    context: Context,
+    viewModel: BackupConfigViewModel,
+    scope: kotlinx.coroutines.CoroutineScope,
+    snackbarHostState: SnackbarHostState,
+    hideLoading: () -> Unit,
+    showLoading: (String) -> Unit
+) {
+    val backupPath = BackupConfig.backupPath
+    if (backupPath.isNullOrEmpty() && mode != "webdav") {
+        return
+    }
+    val path = backupPath ?: ""
+    showLoading(context.getString(R.string.backup))
+    if (path.isNotEmpty() && !path.isContentScheme()) {
+        PermissionsCompat.Builder()
+            .addPermissions(*Permissions.Group.STORAGE)
+            .rationale(R.string.tip_perm_request_storage)
+            .onGranted {
+                startBackup(path, context, viewModel, mode, {
+                    hideLoading()
+                    scope.launch {
+                        snackbarHostState.showSnackbar(context.getString(R.string.backup_success))
+                    }
+                }, { error ->
+                    hideLoading()
+                    scope.launch {
+                        snackbarHostState.showSnackbar(context.getString(R.string.backup_fail, error))
+                    }
+                })
+            }
+            .request()
+    } else {
+        startBackup(path, context, viewModel, mode, {
+            hideLoading()
+            scope.launch {
+                snackbarHostState.showSnackbar(context.getString(R.string.backup_success))
+            }
+        }, { error ->
+            hideLoading()
+            scope.launch {
+                snackbarHostState.showSnackbar(context.getString(R.string.backup_fail, error))
+            }
+        })
+    }
 }

+ 2 - 2
app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt

@@ -46,10 +46,10 @@ class BackupConfigViewModel(
         }
     }
 
-    fun backup(backupPath: String, onSuccess: () -> Unit, onError: (String) -> Unit) {
+    fun backup(backupPath: String, mode: String = "both", onSuccess: () -> Unit, onError: (String) -> Unit) {
         viewModelScope.launch(Dispatchers.IO) {
             try {
-                Backup.backupLocked(appCtx, backupPath)
+                Backup.backupLocked(appCtx, backupPath, mode)
                 withContext(Dispatchers.Main) {
                     onSuccess()
                 }