feat(settings): 支持备份与恢复数据库

This commit is contained in:
Super12138 2025-02-11 17:16:48 +08:00
parent 58f74325c0
commit 4771d48c6b
21 changed files with 389 additions and 19 deletions

View file

@ -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"

View file

@ -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")

View file

@ -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()

View file

@ -7,6 +7,7 @@ object TodoDefaults {
* 屏幕左右两边预留边距防止内容全部贴边显示过丑
*/
val screenPadding = 16.dp
/**
* 待办卡片默认高度
*/

View file

@ -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()
)
}

View file

@ -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
)
}

View file

@ -72,6 +72,7 @@ fun LargeTopAppBarScaffold(
}
},
scrollBehavior = scrollBehavior,
snackbarHost = snackbarHost,
floatingActionButton = floatingActionButton,
floatingActionButtonPosition = floatingActionButtonPosition,
contentWindowInsets = contentWindowInsets

View file

@ -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() },

View file

@ -6,6 +6,7 @@ enum class TodoScreen {
SettingsMain,
SettingsAppearance,
SettingsInterface,
SettingsData,
SettingsAbout,
SettingsAboutLicence
}

View file

@ -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

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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)
}

View file

@ -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),

View file

@ -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),
)

View file

@ -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)
}
}
}
}

View file

@ -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)
}
}

View file

@ -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>

View file

@ -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 &amp; 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 &amp; 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>

View file

@ -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>