diff --git a/app/src/main/java/io/legado/app/help/storage/Backup.kt b/app/src/main/java/io/legado/app/help/storage/Backup.kt index 68588a2d9..06e8427c3 100644 --- a/app/src/main/java/io/legado/app/help/storage/Backup.kt +++ b/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) diff --git a/app/src/main/java/io/legado/app/help/storage/BackupConfig.kt b/app/src/main/java/io/legado/app/help/storage/BackupConfig.kt index b3b57af3e..f0b691372 100644 --- a/app/src/main/java/io/legado/app/help/storage/BackupConfig.kt +++ b/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 diff --git a/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfig.kt b/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfig.kt index d4a42d957..896c1ed37 100644 --- a/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfig.kt +++ b/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( PreferKey.backupPath, null diff --git a/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigScreen.kt b/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigScreen.kt index 869aa374a..5270cc743 100644 --- a/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigScreen.kt +++ b/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>(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)) + } + }) + } } diff --git a/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt b/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt index 4cd0d09f2..aa3e29613 100644 --- a/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt +++ b/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() }