diff --git a/app/src/main/kotlin/cn/super12138/todo/constants/Constants.kt b/app/src/main/kotlin/cn/super12138/todo/constants/Constants.kt index 79d453e..8f56131 100644 --- a/app/src/main/kotlin/cn/super12138/todo/constants/Constants.kt +++ b/app/src/main/kotlin/cn/super12138/todo/constants/Constants.kt @@ -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" diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDao.kt b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDao.kt index 239a664..8ba646a 100644 --- a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDao.kt +++ b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDao.kt @@ -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> @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) /*@Query("DELETE FROM todo") diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt index 8f72c65..e5db67b 100644 --- a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt +++ b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt @@ -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() diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt b/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt index 7b962d4..8a72b54 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/TodoDefaults.kt @@ -7,6 +7,7 @@ object TodoDefaults { * 屏幕左右两边预留边距(防止内容全部贴边显示过丑) */ val screenPadding = 16.dp + /** * 待办卡片默认高度 */ diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt b/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt index e69a0b5..08208c8 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt @@ -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() ) } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt index 25e9966..0dea239 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt @@ -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 ) } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/Scaffolds.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/Scaffolds.kt index 12ece54..7993b25 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/components/Scaffolds.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/Scaffolds.kt @@ -72,6 +72,7 @@ fun LargeTopAppBarScaffold( } }, scrollBehavior = scrollBehavior, + snackbarHost = snackbarHost, floatingActionButton = floatingActionButton, floatingActionButtonPosition = floatingActionButtonPosition, contentWindowInsets = contentWindowInsets diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt index a4fda68..de4ef90 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt @@ -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() }, diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoScreen.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoScreen.kt index 68a5837..fe5b0ee 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoScreen.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoScreen.kt @@ -6,6 +6,7 @@ enum class TodoScreen { SettingsMain, SettingsAppearance, SettingsInterface, + SettingsData, SettingsAbout, SettingsAboutLicence } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/crash/CrashPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/crash/CrashPage.kt index be089fa..0bca644 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/crash/CrashPage.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/crash/CrashPage.kt @@ -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 diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt index e409877..6083b3c 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt @@ -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 { diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ManagerFragment.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ManagerFragment.kt index 91cdd07..0a5d053 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ManagerFragment.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/ManagerFragment.kt @@ -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 { diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoFAB.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoFAB.kt index 4d6ca5f..ad8e5e7 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoFAB.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/components/TodoFAB.kt @@ -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 diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsData.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsData.kt new file mode 100644 index 0000000..f599cf9 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsData.kt @@ -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) +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsMain.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsMain.kt index 25b349e..4640d92 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsMain.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsMain.kt @@ -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), diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt index 8707a16..be4a4b7 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt @@ -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), ) diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt index 6001e8a..a5604e4 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt @@ -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(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) + } + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/SystemUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/SystemUtils.kt index 63401a4..b594f69 100644 --- a/app/src/main/kotlin/cn/super12138/todo/utils/SystemUtils.kt +++ b/app/src/main/kotlin/cn/super12138/todo/utils/SystemUtils.kt @@ -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) + } } \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1db3fdc..ba0fdd2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -94,4 +94,14 @@ 为某些操作提供轻微的震动反馈 提示 使用此项前须先在系统设置中打开触感反馈(通常在“声音与震动”里的“触感反馈”一项) + 数据 + 备份与恢复、管理数据 + 备份数据 + 将待办数据库保存在你的设备上 + 恢复数据 + 从你的设备上恢复待办数据库 + 备份成功 + 备份失败 + 数据恢复成功,需要重启应用以加载数据 + 恢复失败 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 752a26c..72f8b21 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -48,7 +48,7 @@ Urgent Priority Appearance - Theme, color style + Theme, Color style Dynamic Color Use the system wallpaper\'s theme color, available on Android 12+ Palette Style @@ -76,7 +76,7 @@ Very High Change the contrast level Interface & Interaction - To-Do List, haptic feedback + To-Do List, Haptic Feedback Show Completed Tasks Show completed tasks in to-do list To-Do List @@ -95,4 +95,14 @@ Provide slight vibration feedback for some operations Tips 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) + Data + Backup and Restore, Manage Data + Backup + Save the to-do database on your device + Restore + Restore the to-do database from your device + Backup successful + Backup failed + Restore successful, restart the app to load data + Restore failed \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 7a880b5..4bd180c 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,5 +1,6 @@ +