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_FAB_TRANSITION = "todo_fab"
|
||||||
const val KEY_TODO_CONTENT_TRANSITION = "todo_content"
|
const val KEY_TODO_CONTENT_TRANSITION = "todo_content"
|
||||||
|
|
||||||
|
const val DB_NAME = "todo"
|
||||||
|
|
||||||
const val SP_NAME = "cn.super12138.todo_preferences"
|
const val SP_NAME = "cn.super12138.todo_preferences"
|
||||||
|
|
||||||
const val PREF_DYNAMIC_COLOR = "dynamic_color"
|
const val PREF_DYNAMIC_COLOR = "dynamic_color"
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
|
import cn.super12138.todo.constants.Constants
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
|
|
@ -13,7 +14,7 @@ interface TodoDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(toDo: TodoEntity)
|
suspend fun insert(toDo: TodoEntity)
|
||||||
|
|
||||||
@Query("SELECT * FROM todo")
|
@Query("SELECT * FROM ${Constants.DB_NAME}")
|
||||||
fun getAll(): Flow<List<TodoEntity>>
|
fun getAll(): Flow<List<TodoEntity>>
|
||||||
|
|
||||||
@Update
|
@Update
|
||||||
|
|
@ -22,7 +23,7 @@ interface TodoDao {
|
||||||
@Delete
|
@Delete
|
||||||
suspend fun delete(toDo: TodoEntity)
|
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>)
|
suspend fun deleteFromIds(toDoIds: Set<Int>)
|
||||||
|
|
||||||
/*@Query("DELETE FROM todo")
|
/*@Query("DELETE FROM todo")
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import cn.super12138.todo.constants.Constants
|
||||||
|
|
||||||
@Database(entities = [TodoEntity::class], version = 2)
|
@Database(entities = [TodoEntity::class], version = 2)
|
||||||
abstract class TodoDatabase : RoomDatabase() {
|
abstract class TodoDatabase : RoomDatabase() {
|
||||||
|
|
@ -17,7 +18,7 @@ abstract class TodoDatabase : RoomDatabase() {
|
||||||
val instance = Room.databaseBuilder(
|
val instance = Room.databaseBuilder(
|
||||||
context.applicationContext,
|
context.applicationContext,
|
||||||
TodoDatabase::class.java,
|
TodoDatabase::class.java,
|
||||||
"todo"
|
Constants.DB_NAME
|
||||||
)
|
)
|
||||||
.fallbackToDestructiveMigration()
|
.fallbackToDestructiveMigration()
|
||||||
.build()
|
.build()
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ object TodoDefaults {
|
||||||
* 屏幕左右两边预留边距(防止内容全部贴边显示过丑)
|
* 屏幕左右两边预留边距(防止内容全部贴边显示过丑)
|
||||||
*/
|
*/
|
||||||
val screenPadding = 16.dp
|
val screenPadding = 16.dp
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 待办卡片默认高度
|
* 待办卡片默认高度
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,11 @@ package cn.super12138.todo.ui.activities
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.HapticFeedbackConstants
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import cn.super12138.todo.R
|
import cn.super12138.todo.R
|
||||||
import cn.super12138.todo.ui.pages.crash.CrashPage
|
import cn.super12138.todo.ui.pages.crash.CrashPage
|
||||||
|
|
@ -54,13 +52,9 @@ class CrashActivity : ComponentActivity() {
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
ToDoTheme {
|
ToDoTheme {
|
||||||
val view = LocalView.current
|
|
||||||
CrashPage(
|
CrashPage(
|
||||||
crashLog = if (crashLogs == null) stringResource(R.string.tip_no_crash_logs) else crashLogFormatted,
|
crashLog = if (crashLogs == null) stringResource(R.string.tip_no_crash_logs) else crashLogFormatted,
|
||||||
exitApp = {
|
exitApp = { finishAffinity() },
|
||||||
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
|
||||||
finishAffinity()
|
|
||||||
},
|
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import cn.super12138.todo.R
|
import cn.super12138.todo.R
|
||||||
import cn.super12138.todo.utils.VibrationUtils
|
import cn.super12138.todo.utils.VibrationUtils
|
||||||
|
|
||||||
|
|
@ -25,6 +26,7 @@ fun WarningDialog(
|
||||||
description: String,
|
description: String,
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
|
properties: DialogProperties = DialogProperties(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
BasicDialog(
|
BasicDialog(
|
||||||
|
|
@ -39,6 +41,48 @@ fun WarningDialog(
|
||||||
onDismiss()
|
onDismiss()
|
||||||
},
|
},
|
||||||
onDismiss = 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
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -53,6 +97,7 @@ fun BasicDialog(
|
||||||
dismissButton: String,
|
dismissButton: String,
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
|
properties: DialogProperties = DialogProperties(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
|
|
@ -87,6 +132,7 @@ fun BasicDialog(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
|
properties = properties,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -100,6 +146,7 @@ fun BasicDialog(
|
||||||
confirmButton: (@Composable () -> Unit),
|
confirmButton: (@Composable () -> Unit),
|
||||||
dismissButton: (@Composable () -> Unit)? = null,
|
dismissButton: (@Composable () -> Unit)? = null,
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
|
properties: DialogProperties = DialogProperties(),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
|
|
@ -114,6 +161,7 @@ fun BasicDialog(
|
||||||
confirmButton = confirmButton,
|
confirmButton = confirmButton,
|
||||||
dismissButton = dismissButton,
|
dismissButton = dismissButton,
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
properties = properties,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ fun LargeTopAppBarScaffold(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
|
snackbarHost = snackbarHost,
|
||||||
floatingActionButton = floatingActionButton,
|
floatingActionButton = floatingActionButton,
|
||||||
floatingActionButtonPosition = floatingActionButtonPosition,
|
floatingActionButtonPosition = floatingActionButtonPosition,
|
||||||
contentWindowInsets = contentWindowInsets
|
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.SettingsAbout
|
||||||
import cn.super12138.todo.ui.pages.settings.SettingsAboutLicence
|
import cn.super12138.todo.ui.pages.settings.SettingsAboutLicence
|
||||||
import cn.super12138.todo.ui.pages.settings.SettingsAppearance
|
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.SettingsInterface
|
||||||
import cn.super12138.todo.ui.pages.settings.SettingsMain
|
import cn.super12138.todo.ui.pages.settings.SettingsMain
|
||||||
import cn.super12138.todo.ui.theme.materialSharedAxisXIn
|
import cn.super12138.todo.ui.theme.materialSharedAxisXIn
|
||||||
|
|
@ -94,6 +95,7 @@ fun TodoNavigation(
|
||||||
toAppearancePage = { navController.navigate(TodoScreen.SettingsAppearance.name) },
|
toAppearancePage = { navController.navigate(TodoScreen.SettingsAppearance.name) },
|
||||||
toAboutPage = { navController.navigate(TodoScreen.SettingsAbout.name) },
|
toAboutPage = { navController.navigate(TodoScreen.SettingsAbout.name) },
|
||||||
toInterfacePage = { navController.navigate(TodoScreen.SettingsInterface.name) },
|
toInterfacePage = { navController.navigate(TodoScreen.SettingsInterface.name) },
|
||||||
|
toDataPage = { navController.navigate(TodoScreen.SettingsData.name) },
|
||||||
onNavigateUp = { navController.navigateUp() },
|
onNavigateUp = { navController.navigateUp() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -112,6 +114,13 @@ fun TodoNavigation(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable(TodoScreen.SettingsData.name) {
|
||||||
|
SettingsData(
|
||||||
|
viewModel = viewModel,
|
||||||
|
onNavigateUp = { navController.navigateUp() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
composable(TodoScreen.SettingsAbout.name) {
|
composable(TodoScreen.SettingsAbout.name) {
|
||||||
SettingsAbout(
|
SettingsAbout(
|
||||||
onNavigateUp = { navController.navigateUp() },
|
onNavigateUp = { navController.navigateUp() },
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ enum class TodoScreen {
|
||||||
SettingsMain,
|
SettingsMain,
|
||||||
SettingsAppearance,
|
SettingsAppearance,
|
||||||
SettingsInterface,
|
SettingsInterface,
|
||||||
|
SettingsData,
|
||||||
SettingsAbout,
|
SettingsAbout,
|
||||||
SettingsAboutLicence
|
SettingsAboutLicence
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,6 @@ import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.ExitToApp
|
import androidx.compose.material.icons.automirrored.outlined.ExitToApp
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.LargeTopAppBar
|
import androidx.compose.material3.LargeTopAppBar
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
|
|
||||||
|
|
@ -206,7 +206,8 @@ fun TodoEditorPage(
|
||||||
|
|
||||||
Spacer(Modifier.size(5.dp))
|
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() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
Slider(
|
Slider(
|
||||||
modifier = Modifier.semantics {
|
modifier = Modifier.semantics {
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,11 @@ fun ManagerFragment(
|
||||||
) {
|
) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = state,
|
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()) {
|
if (list.isEmpty()) {
|
||||||
item {
|
item {
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,9 @@ import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Add
|
import androidx.compose.material.icons.outlined.Add
|
||||||
import androidx.compose.material3.FloatingActionButtonDefaults
|
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||||
import androidx.compose.material3.FloatingActionButtonElevation
|
import androidx.compose.material3.FloatingActionButtonElevation
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import cn.super12138.todo.R
|
import cn.super12138.todo.R
|
||||||
import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
|
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.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.ColorLens
|
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.Info
|
||||||
import androidx.compose.material.icons.outlined.ViewComfy
|
import androidx.compose.material.icons.outlined.ViewComfy
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
|
@ -23,6 +24,7 @@ import cn.super12138.todo.ui.pages.settings.components.SettingsItem
|
||||||
fun SettingsMain(
|
fun SettingsMain(
|
||||||
toAppearancePage: () -> Unit,
|
toAppearancePage: () -> Unit,
|
||||||
toInterfacePage: () -> Unit,
|
toInterfacePage: () -> Unit,
|
||||||
|
toDataPage: () -> Unit,
|
||||||
toAboutPage: () -> Unit,
|
toAboutPage: () -> Unit,
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateUp: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
|
|
@ -51,6 +53,12 @@ fun SettingsMain(
|
||||||
description = stringResource(R.string.pref_interface_interaction_desc),
|
description = stringResource(R.string.pref_interface_interaction_desc),
|
||||||
onClick = toInterfacePage
|
onClick = toInterfacePage
|
||||||
)
|
)
|
||||||
|
SettingsItem(
|
||||||
|
leadingIcon = Icons.Outlined.Dns,
|
||||||
|
title = stringResource(R.string.pref_data),
|
||||||
|
description = stringResource(R.string.pref_data_desc),
|
||||||
|
onClick = toDataPage
|
||||||
|
)
|
||||||
SettingsItem(
|
SettingsItem(
|
||||||
leadingIcon = Icons.Outlined.Info,
|
leadingIcon = Icons.Outlined.Info,
|
||||||
title = stringResource(R.string.pref_about),
|
title = stringResource(R.string.pref_about),
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ fun SettingsItem(
|
||||||
leadingIcon?.let {
|
leadingIcon?.let {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = it,
|
imageVector = it,
|
||||||
contentDescription = title,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
modifier = Modifier.padding(end = 24.dp),
|
modifier = Modifier.padding(end = 24.dp),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
package cn.super12138.todo.ui.viewmodels
|
package cn.super12138.todo.ui.viewmodels
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
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.constants.GlobalValues
|
||||||
import cn.super12138.todo.logic.Repository
|
import cn.super12138.todo.logic.Repository
|
||||||
import cn.super12138.todo.logic.database.TodoEntity
|
import cn.super12138.todo.logic.database.TodoEntity
|
||||||
import cn.super12138.todo.logic.model.ContrastLevel
|
import cn.super12138.todo.logic.model.ContrastLevel
|
||||||
import cn.super12138.todo.logic.model.DarkMode
|
import cn.super12138.todo.logic.model.DarkMode
|
||||||
import cn.super12138.todo.logic.model.SortingMethod
|
import cn.super12138.todo.logic.model.SortingMethod
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
@ -18,6 +23,15 @@ import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
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() {
|
class MainViewModel : ViewModel() {
|
||||||
// 待办
|
// 待办
|
||||||
|
|
@ -33,6 +47,7 @@ class MainViewModel : ViewModel() {
|
||||||
SortingMethod.AlphabeticalDescending -> list.sortedByDescending { it.content }
|
SortingMethod.AlphabeticalDescending -> list.sortedByDescending { it.content }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val showConfetti = mutableStateOf(false)
|
val showConfetti = mutableStateOf(false)
|
||||||
var selectedEditTodo by mutableStateOf<TodoEntity?>(null)
|
var selectedEditTodo by mutableStateOf<TodoEntity?>(null)
|
||||||
private set
|
private set
|
||||||
|
|
@ -150,4 +165,91 @@ class MainViewModel : ViewModel() {
|
||||||
fun setSortingMethod(sortingMethod: SortingMethod) {
|
fun setSortingMethod(sortingMethod: SortingMethod) {
|
||||||
appSortingMethod = 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.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
object SystemUtils {
|
object SystemUtils {
|
||||||
/**
|
/**
|
||||||
|
|
@ -18,4 +21,22 @@ object SystemUtils {
|
||||||
}
|
}
|
||||||
return "$verName ($verCode)"
|
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="pref_haptic_feedback_desc">为某些操作提供轻微的震动反馈</string>
|
||||||
<string name="tip_tips">提示</string>
|
<string name="tip_tips">提示</string>
|
||||||
<string name="pref_haptic_feedback_more_info">使用此项前须先在系统设置中打开触感反馈(通常在“声音与震动”里的“触感反馈”一项)</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>
|
</resources>
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
<string name="priority_urgent">Urgent</string>
|
<string name="priority_urgent">Urgent</string>
|
||||||
<string name="label_priority">Priority</string>
|
<string name="label_priority">Priority</string>
|
||||||
<string name="pref_appearance">Appearance</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">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_appearance_dynamic_color_desc">Use the system wallpaper\'s theme color, available on Android 12+</string>
|
||||||
<string name="pref_palette_style">Palette Style</string>
|
<string name="pref_palette_style">Palette Style</string>
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
<string name="contrast_very_high">Very High</string>
|
<string name="contrast_very_high">Very High</string>
|
||||||
<string name="tip_change_contrast_level">Change the contrast level</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">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">Show Completed Tasks</string>
|
||||||
<string name="pref_show_completed_desc">Show completed tasks in to-do list</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>
|
<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="pref_haptic_feedback_desc">Provide slight vibration feedback for some operations</string>
|
||||||
<string name="tip_tips">Tips</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_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>
|
</resources>
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<style name="Theme.ToDo.Starting" parent="Theme.SplashScreen">
|
<style name="Theme.ToDo.Starting" parent="Theme.SplashScreen">
|
||||||
<item name="windowSplashScreenBackground">@color/background</item>
|
<item name="windowSplashScreenBackground">@color/background</item>
|
||||||
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
|
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue