备份可选网络、本地、网络与本地同时备份
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,6 +50,11 @@ object BackupConfig {
|
||||
true
|
||||
)
|
||||
|
||||
var backupSyncMode by prefDelegate(
|
||||
PreferKey.backupSyncMode,
|
||||
"both"
|
||||
)
|
||||
|
||||
var backupPath by prefDelegate<String?>(
|
||||
PreferKey.backupPath,
|
||||
null
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user