feat(settings): 支持备份与恢复数据库
This commit is contained in:
parent
58f74325c0
commit
4771d48c6b
21 changed files with 389 additions and 19 deletions
|
|
@ -7,6 +7,8 @@ object Constants {
|
|||
const val KEY_TODO_FAB_TRANSITION = "todo_fab"
|
||||
const val KEY_TODO_CONTENT_TRANSITION = "todo_content"
|
||||
|
||||
const val DB_NAME = "todo"
|
||||
|
||||
const val SP_NAME = "cn.super12138.todo_preferences"
|
||||
|
||||
const val PREF_DYNAMIC_COLOR = "dynamic_color"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import androidx.room.Insert
|
|||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Update
|
||||
import cn.super12138.todo.constants.Constants
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
|
|
@ -13,7 +14,7 @@ interface TodoDao {
|
|||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(toDo: TodoEntity)
|
||||
|
||||
@Query("SELECT * FROM todo")
|
||||
@Query("SELECT * FROM ${Constants.DB_NAME}")
|
||||
fun getAll(): Flow<List<TodoEntity>>
|
||||
|
||||
@Update
|
||||
|
|
@ -22,7 +23,7 @@ interface TodoDao {
|
|||
@Delete
|
||||
suspend fun delete(toDo: TodoEntity)
|
||||
|
||||
@Query("DELETE FROM todo WHERE id in (:toDoIds)")
|
||||
@Query("DELETE FROM ${Constants.DB_NAME} WHERE id in (:toDoIds)")
|
||||
suspend fun deleteFromIds(toDoIds: Set<Int>)
|
||||
|
||||
/*@Query("DELETE FROM todo")
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import cn.super12138.todo.constants.Constants
|
||||
|
||||
@Database(entities = [TodoEntity::class], version = 2)
|
||||
abstract class TodoDatabase : RoomDatabase() {
|
||||
|
|
@ -17,7 +18,7 @@ abstract class TodoDatabase : RoomDatabase() {
|
|||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
TodoDatabase::class.java,
|
||||
"todo"
|
||||
Constants.DB_NAME
|
||||
)
|
||||
.fallbackToDestructiveMigration()
|
||||
.build()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ object TodoDefaults {
|
|||
* 屏幕左右两边预留边距(防止内容全部贴边显示过丑)
|
||||
*/
|
||||
val screenPadding = 16.dp
|
||||
|
||||
/**
|
||||
* 待办卡片默认高度
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -2,13 +2,11 @@ package cn.super12138.todo.ui.activities
|
|||
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.HapticFeedbackConstants
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.ui.pages.crash.CrashPage
|
||||
|
|
@ -54,13 +52,9 @@ class CrashActivity : ComponentActivity() {
|
|||
|
||||
setContent {
|
||||
ToDoTheme {
|
||||
val view = LocalView.current
|
||||
CrashPage(
|
||||
crashLog = if (crashLogs == null) stringResource(R.string.tip_no_crash_logs) else crashLogFormatted,
|
||||
exitApp = {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
finishAffinity()
|
||||
},
|
||||
exitApp = { finishAffinity() },
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
|
|||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
|
||||
|
|
@ -25,6 +26,7 @@ fun WarningDialog(
|
|||
description: String,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
properties: DialogProperties = DialogProperties(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
BasicDialog(
|
||||
|
|
@ -39,6 +41,48 @@ fun WarningDialog(
|
|||
onDismiss()
|
||||
},
|
||||
onDismiss = onDismiss,
|
||||
properties = properties,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ConfirmDialog(
|
||||
visible: Boolean,
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
text: String,
|
||||
confirmButton: String,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
properties: DialogProperties = DialogProperties(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val view = LocalView.current
|
||||
BasicDialog(
|
||||
visible = visible,
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null // 会跟下面的文本重复,所以设置为 null
|
||||
)
|
||||
},
|
||||
title = { Text(title) },
|
||||
text = { Text(text) },
|
||||
confirmButton = {
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
VibrationUtils.performHapticFeedback(view)
|
||||
onConfirm()
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text(confirmButton)
|
||||
}
|
||||
},
|
||||
dismissButton = {},
|
||||
onDismissRequest = onDismiss,
|
||||
properties = properties,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
@ -53,6 +97,7 @@ fun BasicDialog(
|
|||
dismissButton: String,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
properties: DialogProperties = DialogProperties(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val view = LocalView.current
|
||||
|
|
@ -87,6 +132,7 @@ fun BasicDialog(
|
|||
}
|
||||
},
|
||||
onDismissRequest = onDismiss,
|
||||
properties = properties,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
@ -100,6 +146,7 @@ fun BasicDialog(
|
|||
confirmButton: (@Composable () -> Unit),
|
||||
dismissButton: (@Composable () -> Unit)? = null,
|
||||
onDismissRequest: () -> Unit,
|
||||
properties: DialogProperties = DialogProperties(),
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (visible) {
|
||||
|
|
@ -114,6 +161,7 @@ fun BasicDialog(
|
|||
confirmButton = confirmButton,
|
||||
dismissButton = dismissButton,
|
||||
onDismissRequest = onDismissRequest,
|
||||
properties = properties,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ fun LargeTopAppBarScaffold(
|
|||
}
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
snackbarHost = snackbarHost,
|
||||
floatingActionButton = floatingActionButton,
|
||||
floatingActionButtonPosition = floatingActionButtonPosition,
|
||||
contentWindowInsets = contentWindowInsets
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import cn.super12138.todo.ui.pages.main.MainPage
|
|||
import cn.super12138.todo.ui.pages.settings.SettingsAbout
|
||||
import cn.super12138.todo.ui.pages.settings.SettingsAboutLicence
|
||||
import cn.super12138.todo.ui.pages.settings.SettingsAppearance
|
||||
import cn.super12138.todo.ui.pages.settings.SettingsData
|
||||
import cn.super12138.todo.ui.pages.settings.SettingsInterface
|
||||
import cn.super12138.todo.ui.pages.settings.SettingsMain
|
||||
import cn.super12138.todo.ui.theme.materialSharedAxisXIn
|
||||
|
|
@ -94,6 +95,7 @@ fun TodoNavigation(
|
|||
toAppearancePage = { navController.navigate(TodoScreen.SettingsAppearance.name) },
|
||||
toAboutPage = { navController.navigate(TodoScreen.SettingsAbout.name) },
|
||||
toInterfacePage = { navController.navigate(TodoScreen.SettingsInterface.name) },
|
||||
toDataPage = { navController.navigate(TodoScreen.SettingsData.name) },
|
||||
onNavigateUp = { navController.navigateUp() },
|
||||
)
|
||||
}
|
||||
|
|
@ -112,6 +114,13 @@ fun TodoNavigation(
|
|||
)
|
||||
}
|
||||
|
||||
composable(TodoScreen.SettingsData.name) {
|
||||
SettingsData(
|
||||
viewModel = viewModel,
|
||||
onNavigateUp = { navController.navigateUp() }
|
||||
)
|
||||
}
|
||||
|
||||
composable(TodoScreen.SettingsAbout.name) {
|
||||
SettingsAbout(
|
||||
onNavigateUp = { navController.navigateUp() },
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ enum class TodoScreen {
|
|||
SettingsMain,
|
||||
SettingsAppearance,
|
||||
SettingsInterface,
|
||||
SettingsData,
|
||||
SettingsAbout,
|
||||
SettingsAboutLicence
|
||||
}
|
||||
|
|
@ -13,7 +13,6 @@ import androidx.compose.foundation.verticalScroll
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ExitToApp
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LargeTopAppBar
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
|
|
|
|||
|
|
@ -206,7 +206,8 @@ fun TodoEditorPage(
|
|||
|
||||
Spacer(Modifier.size(5.dp))
|
||||
|
||||
val priorityName = Priority.entries.map { it.getDisplayName(context) } // 要不要用 remember 呢
|
||||
val priorityName =
|
||||
Priority.entries.map { it.getDisplayName(context) } // 要不要用 remember 呢
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
Slider(
|
||||
modifier = Modifier.semantics {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,11 @@ fun ManagerFragment(
|
|||
) {
|
||||
LazyColumn(
|
||||
state = state,
|
||||
contentPadding = PaddingValues(start = TodoDefaults.screenPadding, bottom = TodoDefaults.toDoCardHeight / 2, end = TodoDefaults.screenPadding)
|
||||
contentPadding = PaddingValues(
|
||||
start = TodoDefaults.screenPadding,
|
||||
bottom = TodoDefaults.toDoCardHeight / 2,
|
||||
end = TodoDefaults.screenPadding
|
||||
)
|
||||
) {
|
||||
if (list.isEmpty()) {
|
||||
item {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,9 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.outlined.Add
|
||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||
import androidx.compose.material3.FloatingActionButtonElevation
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,159 @@
|
|||
package cn.super12138.todo.ui.pages.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.FileDownload
|
||||
import androidx.compose.material.icons.outlined.FileUpload
|
||||
import androidx.compose.material.icons.outlined.RestartAlt
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.ui.Modifier
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import cn.super12138.todo.R
|
||||
import cn.super12138.todo.ui.activities.MainActivity
|
||||
import cn.super12138.todo.ui.components.ConfirmDialog
|
||||
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
|
||||
import cn.super12138.todo.ui.pages.settings.components.SettingsItem
|
||||
import cn.super12138.todo.ui.viewmodels.MainViewModel
|
||||
import cn.super12138.todo.utils.SystemUtils
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsData(
|
||||
viewModel: MainViewModel,
|
||||
onNavigateUp: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var showRestoreDialog by rememberSaveable { mutableStateOf(false) }
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val backupLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.CreateDocument("application/zip"),
|
||||
onResult = {
|
||||
if (it != null) {
|
||||
viewModel.backupDatabase(
|
||||
uri = it,
|
||||
context = context,
|
||||
onResult = { success ->
|
||||
if (success) {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.tip_backup_success)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.tip_backup_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
val restoreLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument(),
|
||||
onResult = { it ->
|
||||
if (it != null) {
|
||||
viewModel.restoreDatabase(
|
||||
uri = it,
|
||||
context = context,
|
||||
onResult = { success ->
|
||||
if (success) {
|
||||
showRestoreDialog = true
|
||||
} else {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.tip_restore_failed)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
LargeTopAppBarScaffold(
|
||||
title = stringResource(R.string.pref_data),
|
||||
onBack = onNavigateUp,
|
||||
scrollBehavior = scrollBehavior,
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
SettingsItem(
|
||||
leadingIcon = Icons.Outlined.FileDownload,
|
||||
title = stringResource(R.string.pref_backup),
|
||||
description = stringResource(R.string.pref_backup_desc),
|
||||
onClick = {
|
||||
backupLauncher.launch("Todo-backup-${SystemUtils.getTime()}.zip")
|
||||
}
|
||||
)
|
||||
SettingsItem(
|
||||
leadingIcon = Icons.Outlined.FileUpload,
|
||||
title = stringResource(R.string.pref_restore),
|
||||
description = stringResource(R.string.pref_restore_desc),
|
||||
onClick = {
|
||||
restoreLauncher.launch(arrayOf("application/zip"))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
ConfirmDialog(
|
||||
visible = showRestoreDialog,
|
||||
icon = Icons.Outlined.RestartAlt,
|
||||
title = stringResource(R.string.tip_tips),
|
||||
text = stringResource(R.string.tip_restore_success),
|
||||
confirmButton = stringResource(R.string.action_confirm),
|
||||
onConfirm = { restartApp(context) },
|
||||
onDismiss = { showRestoreDialog = false },
|
||||
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false)
|
||||
)
|
||||
}
|
||||
|
||||
private fun restartApp(context: Context) {
|
||||
val intent = Intent(
|
||||
context,
|
||||
MainActivity::class.java
|
||||
).apply {
|
||||
flags =
|
||||
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
context.startActivity(intent)
|
||||
exitProcess(0)
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.rememberScrollState
|
|||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.ColorLens
|
||||
import androidx.compose.material.icons.outlined.Dns
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material.icons.outlined.ViewComfy
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
|
|
@ -23,6 +24,7 @@ import cn.super12138.todo.ui.pages.settings.components.SettingsItem
|
|||
fun SettingsMain(
|
||||
toAppearancePage: () -> Unit,
|
||||
toInterfacePage: () -> Unit,
|
||||
toDataPage: () -> Unit,
|
||||
toAboutPage: () -> Unit,
|
||||
onNavigateUp: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
|
|
@ -51,6 +53,12 @@ fun SettingsMain(
|
|||
description = stringResource(R.string.pref_interface_interaction_desc),
|
||||
onClick = toInterfacePage
|
||||
)
|
||||
SettingsItem(
|
||||
leadingIcon = Icons.Outlined.Dns,
|
||||
title = stringResource(R.string.pref_data),
|
||||
description = stringResource(R.string.pref_data_desc),
|
||||
onClick = toDataPage
|
||||
)
|
||||
SettingsItem(
|
||||
leadingIcon = Icons.Outlined.Info,
|
||||
title = stringResource(R.string.pref_about),
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ fun SettingsItem(
|
|||
leadingIcon?.let {
|
||||
Icon(
|
||||
imageVector = it,
|
||||
contentDescription = title,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(end = 24.dp),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
package cn.super12138.todo.ui.viewmodels
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cn.super12138.todo.TodoApp
|
||||
import cn.super12138.todo.constants.Constants
|
||||
import cn.super12138.todo.constants.GlobalValues
|
||||
import cn.super12138.todo.logic.Repository
|
||||
import cn.super12138.todo.logic.database.TodoEntity
|
||||
import cn.super12138.todo.logic.model.ContrastLevel
|
||||
import cn.super12138.todo.logic.model.DarkMode
|
||||
import cn.super12138.todo.logic.model.SortingMethod
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -18,6 +23,15 @@ import kotlinx.coroutines.flow.firstOrNull
|
|||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.BufferedOutputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
class MainViewModel : ViewModel() {
|
||||
// 待办
|
||||
|
|
@ -33,6 +47,7 @@ class MainViewModel : ViewModel() {
|
|||
SortingMethod.AlphabeticalDescending -> list.sortedByDescending { it.content }
|
||||
}
|
||||
}
|
||||
|
||||
val showConfetti = mutableStateOf(false)
|
||||
var selectedEditTodo by mutableStateOf<TodoEntity?>(null)
|
||||
private set
|
||||
|
|
@ -150,4 +165,91 @@ class MainViewModel : ViewModel() {
|
|||
fun setSortingMethod(sortingMethod: SortingMethod) {
|
||||
appSortingMethod = sortingMethod
|
||||
}
|
||||
|
||||
fun backupDatabase(uri: Uri, context: Context, onResult: (completed: Boolean) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// 获取数据库文件路径
|
||||
val dbPath = TodoApp.db.openHelper.writableDatabase.path
|
||||
// 开启输出文件流,输出位置为用户选取的文件夹URI
|
||||
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||
ZipOutputStream(BufferedOutputStream(outputStream)).use { zipOutStream ->
|
||||
val dbFile = context.getDatabasePath(Constants.DB_NAME)
|
||||
val dbWal = File("$dbPath-wal")
|
||||
val dbShm = File("$dbPath-shm")
|
||||
|
||||
addFileToZip(dbFile, dbFile.name, zipOutStream)
|
||||
addFileToZip(dbWal, dbWal.name, zipOutStream)
|
||||
addFileToZip(dbShm, dbShm.name, zipOutStream)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行成功的回调
|
||||
withContext(Dispatchers.Main) {
|
||||
onResult(true) // 成功
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
// 执行失败的回调
|
||||
withContext(Dispatchers.Main) {
|
||||
onResult(false) // 失败
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun restoreDatabase(uri: Uri, context: Context, onResult: (completed: Boolean) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// 获取数据库文件路径
|
||||
val outputPath = context.getDatabasePath(Constants.DB_NAME).parent
|
||||
context.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
ZipInputStream(BufferedInputStream(inputStream)).use { zipInputStream ->
|
||||
var zipEntry: ZipEntry? = zipInputStream.nextEntry
|
||||
while (zipEntry != null) {
|
||||
val outputFile = File(outputPath, zipEntry.name)
|
||||
if (zipEntry.isDirectory) {
|
||||
outputFile.mkdirs()
|
||||
} else {
|
||||
outputFile.parentFile?.mkdirs()
|
||||
FileOutputStream(outputFile).use { zipInputStream.copyTo(it) }
|
||||
}
|
||||
zipInputStream.closeEntry()
|
||||
zipEntry = zipInputStream.nextEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
onResult(true) // 成功
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
onResult(false) // 失败
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addFileToZip(file: File, fileName: String, zipOut: ZipOutputStream) {
|
||||
if (file.isHidden) {
|
||||
return // 忽略隐藏文件
|
||||
}
|
||||
if (file.isDirectory) {
|
||||
// 如果是文件夹,递归处理
|
||||
val children = file.listFiles()
|
||||
if (children != null) {
|
||||
for (childFile in children) {
|
||||
addFileToZip(childFile, "$fileName/${childFile.name}", zipOut)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果是文件,写入 ZIP
|
||||
FileInputStream(file).use { fis ->
|
||||
val zipEntry = ZipEntry(fileName)
|
||||
zipOut.putNextEntry(zipEntry)
|
||||
fis.copyTo(zipOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,9 @@ package cn.super12138.todo.utils
|
|||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
object SystemUtils {
|
||||
/**
|
||||
|
|
@ -18,4 +21,22 @@ object SystemUtils {
|
|||
}
|
||||
return "$verName ($verCode)"
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取格式化后的当前时间
|
||||
* 参考 https://github.com/rafi0101/Android-Room-Database-Backup/blob/master/core/src/main/java/de/raphaelebner/roomdatabasebackup/core/RoomBackup.kt#L770
|
||||
* @return 当前时间
|
||||
*/
|
||||
fun getTime(): String {
|
||||
val currentTime = Calendar.getInstance().time
|
||||
|
||||
val sdf =
|
||||
if (Build.VERSION.SDK_INT <= 28) {
|
||||
SimpleDateFormat("yyyy-MM-dd-HH_mm_ss", Locale.getDefault())
|
||||
} else {
|
||||
SimpleDateFormat("yyyy-MM-dd-HH:mm:ss", Locale.getDefault())
|
||||
}
|
||||
|
||||
return sdf.format(currentTime)
|
||||
}
|
||||
}
|
||||
|
|
@ -94,4 +94,14 @@
|
|||
<string name="pref_haptic_feedback_desc">为某些操作提供轻微的震动反馈</string>
|
||||
<string name="tip_tips">提示</string>
|
||||
<string name="pref_haptic_feedback_more_info">使用此项前须先在系统设置中打开触感反馈(通常在“声音与震动”里的“触感反馈”一项)</string>
|
||||
<string name="pref_data">数据</string>
|
||||
<string name="pref_data_desc">备份与恢复、管理数据</string>
|
||||
<string name="pref_backup">备份数据</string>
|
||||
<string name="pref_backup_desc">将待办数据库保存在你的设备上</string>
|
||||
<string name="pref_restore">恢复数据</string>
|
||||
<string name="pref_restore_desc">从你的设备上恢复待办数据库</string>
|
||||
<string name="tip_backup_success">备份成功</string>
|
||||
<string name="tip_backup_failed">备份失败</string>
|
||||
<string name="tip_restore_success">数据恢复成功,需要重启应用以加载数据</string>
|
||||
<string name="tip_restore_failed">恢复失败</string>
|
||||
</resources>
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
<string name="priority_urgent">Urgent</string>
|
||||
<string name="label_priority">Priority</string>
|
||||
<string name="pref_appearance">Appearance</string>
|
||||
<string name="pref_appearance_desc">Theme, color style</string>
|
||||
<string name="pref_appearance_desc">Theme, Color style</string>
|
||||
<string name="pref_appearance_dynamic_color">Dynamic Color</string>
|
||||
<string name="pref_appearance_dynamic_color_desc">Use the system wallpaper\'s theme color, available on Android 12+</string>
|
||||
<string name="pref_palette_style">Palette Style</string>
|
||||
|
|
@ -76,7 +76,7 @@
|
|||
<string name="contrast_very_high">Very High</string>
|
||||
<string name="tip_change_contrast_level">Change the contrast level</string>
|
||||
<string name="pref_interface_interaction">Interface & Interaction</string>
|
||||
<string name="pref_interface_interaction_desc">To-Do List, haptic feedback</string>
|
||||
<string name="pref_interface_interaction_desc">To-Do List, Haptic Feedback</string>
|
||||
<string name="pref_show_completed">Show Completed Tasks</string>
|
||||
<string name="pref_show_completed_desc">Show completed tasks in to-do list</string>
|
||||
<string name="pref_category_todo_list">To-Do List</string>
|
||||
|
|
@ -95,4 +95,14 @@
|
|||
<string name="pref_haptic_feedback_desc">Provide slight vibration feedback for some operations</string>
|
||||
<string name="tip_tips">Tips</string>
|
||||
<string name="pref_haptic_feedback_more_info">Before using this feature, you need to enable haptic feedback in the system settings (usually under the \\\"Sound & Vibration\\\" section in the \\\"Haptic Feedback\\\" option)</string>
|
||||
<string name="pref_data">Data</string>
|
||||
<string name="pref_data_desc">Backup and Restore, Manage Data</string>
|
||||
<string name="pref_backup">Backup</string>
|
||||
<string name="pref_backup_desc">Save the to-do database on your device</string>
|
||||
<string name="pref_restore">Restore</string>
|
||||
<string name="pref_restore_desc">Restore the to-do database from your device</string>
|
||||
<string name="tip_backup_success">Backup successful</string>
|
||||
<string name="tip_backup_failed">Backup failed</string>
|
||||
<string name="tip_restore_success">Restore successful, restart the app to load data</string>
|
||||
<string name="tip_restore_failed">Restore failed</string>
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.ToDo.Starting" parent="Theme.SplashScreen">
|
||||
<item name="windowSplashScreenBackground">@color/background</item>
|
||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
|
||||
|
|
|
|||
Loading…
Reference in a new issue