refactor: 用 Preference DataStore 代替 Preference

This commit is contained in:
Super12138 2025-06-27 14:57:29 +08:00
parent 65258660cd
commit d7eec36fc8
25 changed files with 344 additions and 390 deletions

View file

@ -85,6 +85,7 @@ dependencies {
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.compose.runtime.livedata)
implementation(libs.androidx.datastore.preferences)
// Compose
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))

View file

@ -1,38 +0,0 @@
package cn.super12138.todo.constants
import cn.super12138.todo.utils.SPDelegates
object GlobalValues {
var dynamicColor: Boolean by SPDelegates(
key = Constants.PREF_DYNAMIC_COLOR,
default = Constants.PREF_DYNAMIC_COLOR_DEFAULT
)
var paletteStyle: Int by SPDelegates(
key = Constants.PREF_PALETTE_STYLE,
default = Constants.PREF_PALETTE_STYLE_DEFAULT
)
var darkMode: Int by SPDelegates(
key = Constants.PREF_DARK_MODE,
default = Constants.PREF_DARK_MODE_DEFAULT
)
var contrastLevel: Float by SPDelegates(
key = Constants.PREF_CONTRAST_LEVEL,
default = Constants.PREF_CONTRAST_LEVEL_DEFAULT
)
var showCompleted: Boolean by SPDelegates(
key = Constants.PREF_SHOW_COMPLETED,
default = Constants.PREF_SHOW_COMPLETED_DEFAULT
)
var sortingMethod: Int by SPDelegates(
key = Constants.PREF_SORTING_METHOD,
default = Constants.PREF_SORTING_METHOD_DEFAULT
)
var secureMode: Boolean by SPDelegates(
key = Constants.PREF_SECURE_MODE,
default = Constants.PREF_SECURE_MODE_DEFAULT
)
var hapticFeedback: Boolean by SPDelegates(
key = Constants.PREF_HAPTIC_FEEDBACK,
default = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT
)
}

View file

@ -0,0 +1,124 @@
package cn.super12138.todo.logic.datastore
import android.content.Context
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import cn.super12138.todo.TodoApp
import cn.super12138.todo.constants.Constants
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
object DataStoreManager {
private val Context.dataStore by preferencesDataStore(
name = Constants.SP_NAME,
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(
context = context,
sharedPreferencesName = Constants.SP_NAME,
)
)
}
)
val dataStore = TodoApp.context.dataStore
// Keys
// 外观与个性化
private val DYNAMIC_COLOR = booleanPreferencesKey(Constants.PREF_DYNAMIC_COLOR)
private val PALETTE_STYLE = intPreferencesKey(Constants.PREF_PALETTE_STYLE)
private val DARK_MODE = intPreferencesKey(Constants.PREF_DARK_MODE)
private val CONTRAST_LEVEL = floatPreferencesKey(Constants.PREF_CONTRAST_LEVEL)
// 界面与交互
private val SHOW_COMPLETED = booleanPreferencesKey(Constants.PREF_SHOW_COMPLETED)
private val SORTING_METHOD = intPreferencesKey(Constants.PREF_SORTING_METHOD)
private val SECURE_MODE = booleanPreferencesKey(Constants.PREF_SECURE_MODE)
private val HAPTIC_FEEDBACK = booleanPreferencesKey(Constants.PREF_HAPTIC_FEEDBACK)
// Getters
val dynamicColorFlow: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[DYNAMIC_COLOR] ?: Constants.PREF_DYNAMIC_COLOR_DEFAULT
}
val paletteStyleFlow = dataStore.data.map { preferences ->
preferences[PALETTE_STYLE] ?: Constants.PREF_PALETTE_STYLE_DEFAULT
}
val darkModeFlow = dataStore.data.map { preferences ->
preferences[DARK_MODE] ?: Constants.PREF_DARK_MODE_DEFAULT
}
val contrastLevelFlow = dataStore.data.map { preferences ->
preferences[CONTRAST_LEVEL] ?: Constants.PREF_CONTRAST_LEVEL_DEFAULT
}
val showCompletedFlow: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[SHOW_COMPLETED] ?: Constants.PREF_SHOW_COMPLETED_DEFAULT
}
val sortingMethodFlow: Flow<Int> = dataStore.data.map { preferences ->
preferences[SORTING_METHOD] ?: Constants.PREF_SORTING_METHOD_DEFAULT
}
val secureModeFlow: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[SECURE_MODE] ?: Constants.PREF_SECURE_MODE_DEFAULT
}
val hapticFeedbackFlow: Flow<Boolean> = dataStore.data.map { preferences ->
preferences[HAPTIC_FEEDBACK] ?: Constants.PREF_HAPTIC_FEEDBACK_DEFAULT
}
// Setters
suspend fun setDynamicColor(value: Boolean) {
dataStore.edit { preferences ->
preferences[DYNAMIC_COLOR] = value
}
}
suspend fun setPaletteStyle(value: Int) {
dataStore.edit { preferences ->
preferences[PALETTE_STYLE] = value
}
}
suspend fun setDarkMode(value: Int) {
dataStore.edit { preferences ->
preferences[DARK_MODE] = value
}
}
suspend fun setContrastLevel(value: Float) {
dataStore.edit { preferences ->
preferences[CONTRAST_LEVEL] = value
}
}
suspend fun setShowCompleted(value: Boolean) {
dataStore.edit { preferences ->
preferences[SHOW_COMPLETED] = value
}
}
suspend fun setSortingMethod(value: Int) {
dataStore.edit { preferences ->
preferences[SORTING_METHOD] = value
}
}
suspend fun setSecureMode(value: Boolean) {
dataStore.edit { preferences ->
preferences[SECURE_MODE] = value
}
}
suspend fun setHapticFeedback(value: Boolean) {
dataStore.edit { preferences ->
preferences[HAPTIC_FEEDBACK] = value
}
}
}

View file

@ -8,18 +8,19 @@ import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.core.view.WindowCompat
import cn.super12138.todo.R
import cn.super12138.todo.constants.GlobalValues
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.ui.pages.crash.CrashPage
import cn.super12138.todo.ui.theme.DarkMode
import cn.super12138.todo.ui.theme.DarkMode.Dark
import cn.super12138.todo.ui.theme.DarkMode.FollowSystem
import cn.super12138.todo.ui.theme.DarkMode.Light
import cn.super12138.todo.ui.theme.PaletteStyle
import cn.super12138.todo.ui.theme.ToDoTheme
import cn.super12138.todo.utils.VibrationUtils
class CrashActivity : ComponentActivity() {
companion object {
@ -40,11 +41,16 @@ class CrashActivity : ComponentActivity() {
val crashLogs = intent.getStringExtra("crash_logs")
setContent {
val darkMode = DarkMode.fromId(GlobalValues.darkMode)
val darkTheme = when (darkMode) {
FollowSystem -> isSystemInDarkTheme()
Light -> false
Dark -> true
val dynamicColor by DataStoreManager.dynamicColorFlow.collectAsState(initial = Constants.PREF_DYNAMIC_COLOR_DEFAULT)
val paletteStyle by DataStoreManager.paletteStyleFlow.collectAsState(initial = Constants.PREF_PALETTE_STYLE_DEFAULT)
val contrastLevel by DataStoreManager.contrastLevelFlow.collectAsState(initial = Constants.PREF_CONTRAST_LEVEL_DEFAULT)
val darkMode by DataStoreManager.darkModeFlow.collectAsState(initial = Constants.PREF_DARK_MODE_DEFAULT)
val hapticFeedback by DataStoreManager.hapticFeedbackFlow.collectAsState(initial = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT)
val darkTheme = when (DarkMode.fromId(darkMode)) {
DarkMode.FollowSystem -> isSystemInDarkTheme()
DarkMode.Light -> false
DarkMode.Dark -> true
}
// 配置状态栏和底部导航栏的颜色(在用户切换深色模式时)
// https://github.com/dn0ne/lotus/blob/master/app/src/main/java/com/dn0ne/player/MainActivity.kt#L266
@ -55,11 +61,15 @@ class CrashActivity : ComponentActivity() {
}
}
LaunchedEffect(hapticFeedback) {
VibrationUtils.setEnabled(hapticFeedback)
}
ToDoTheme(
darkTheme = darkTheme,
style = PaletteStyle.fromId(GlobalValues.paletteStyle),
contrastLevel = GlobalValues.contrastLevel.toDouble(),
dynamicColor = GlobalValues.dynamicColor
style = PaletteStyle.fromId(paletteStyle),
contrastLevel = contrastLevel.toDouble(),
dynamicColor = dynamicColor
) {
CrashPage(
crashLog = crashLogs ?: stringResource(R.string.tip_no_crash_logs),

View file

@ -11,17 +11,21 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.ui.components.Konfetti
import cn.super12138.todo.ui.navigation.TodoNavigation
import cn.super12138.todo.ui.theme.DarkMode.Dark
import cn.super12138.todo.ui.theme.DarkMode.FollowSystem
import cn.super12138.todo.ui.theme.DarkMode.Light
import cn.super12138.todo.ui.theme.DarkMode
import cn.super12138.todo.ui.theme.PaletteStyle
import cn.super12138.todo.ui.theme.ToDoTheme
import cn.super12138.todo.ui.viewmodels.MainViewModel
import cn.super12138.todo.utils.VibrationUtils
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@ -34,23 +38,32 @@ class MainActivity : ComponentActivity() {
setContent {
val mainViewModel: MainViewModel = viewModel()
val showConfetti = mainViewModel.showConfetti
// 主题
val dynamicColor by DataStoreManager.dynamicColorFlow.collectAsState(initial = Constants.PREF_DYNAMIC_COLOR_DEFAULT)
val paletteStyle by DataStoreManager.paletteStyleFlow.collectAsState(initial = Constants.PREF_PALETTE_STYLE_DEFAULT)
val contrastLevel by DataStoreManager.contrastLevelFlow.collectAsState(initial = Constants.PREF_CONTRAST_LEVEL_DEFAULT)
val darkMode by DataStoreManager.darkModeFlow.collectAsState(initial = Constants.PREF_DARK_MODE_DEFAULT)
val secureMode by DataStoreManager.secureModeFlow.collectAsState(initial = Constants.PREF_SECURE_MODE_DEFAULT)
val hapticFeedback by DataStoreManager.hapticFeedbackFlow.collectAsState(initial = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT)
// 深色模式
val darkTheme = when (mainViewModel.appDarkMode) {
FollowSystem -> isSystemInDarkTheme()
Light -> false
Dark -> true
val darkTheme = when (DarkMode.fromId(darkMode)) {
DarkMode.FollowSystem -> isSystemInDarkTheme()
DarkMode.Light -> false
DarkMode.Dark -> true
}
// 配置状态栏和底部导航栏的颜色(在用户切换深色模式时)
// https://github.com/dn0ne/lotus/blob/master/app/src/main/java/com/dn0ne/player/MainActivity.kt#L266
LaunchedEffect(mainViewModel.appDarkMode) {
LaunchedEffect(darkMode) {
WindowCompat.getInsetsController(window, window.decorView).apply {
isAppearanceLightStatusBars = !darkTheme
isAppearanceLightNavigationBars = !darkTheme
}
}
// 阻止截屏相关配置
LaunchedEffect(mainViewModel.appSecureMode) {
if (mainViewModel.appSecureMode) {
LaunchedEffect(secureMode) {
if (secureMode) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
@ -60,11 +73,15 @@ class MainActivity : ComponentActivity() {
}
}
LaunchedEffect(hapticFeedback) {
VibrationUtils.setEnabled(hapticFeedback)
}
ToDoTheme(
darkTheme = darkTheme,
style = mainViewModel.appPaletteStyle,
contrastLevel = mainViewModel.appContrastLevel.value.toDouble(),
dynamicColor = mainViewModel.appDynamicColorEnable
style = PaletteStyle.fromId(paletteStyle),
contrastLevel = contrastLevel.toDouble(),
dynamicColor = dynamicColor
) {
Surface(color = MaterialTheme.colorScheme.background) {
TodoNavigation(

View file

@ -102,17 +102,11 @@ fun TodoNavigation(
}
composable(TodoScreen.SettingsAppearance.name) {
SettingsAppearance(
viewModel = viewModel,
onNavigateUp = { navController.navigateUp() }
)
SettingsAppearance(onNavigateUp = { navController.navigateUp() })
}
composable(TodoScreen.SettingsInterface.name) {
SettingsInterface(
viewModel = viewModel,
onNavigateUp = { navController.navigateUp() }
)
SettingsInterface(onNavigateUp = { navController.navigateUp() })
}
composable(TodoScreen.SettingsData.name) {
@ -135,9 +129,7 @@ fun TodoNavigation(
}
composable(TodoScreen.SettingsAboutLicence.name) {
SettingsAboutLicence(
onNavigateUp = { navController.navigateUp() }
)
SettingsAboutLicence(onNavigateUp = { navController.navigateUp() })
}
}
}

View file

@ -33,6 +33,7 @@ import androidx.window.core.layout.WindowWidthSizeClass
import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.database.TodoEntity
import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
import cn.super12138.todo.ui.components.ConfirmDialog
import cn.super12138.todo.ui.pages.main.components.TodoTopAppBar
@ -72,7 +73,8 @@ fun MainPage(
val toDoList by remember { derivedStateOf { toDos.value } }
val showCompleted = viewModel.showCompletedTodos
// Theme
val showCompleted by DataStoreManager.showCompletedFlow.collectAsState(initial = Constants.PREF_SHOW_COMPLETED_DEFAULT)
val filteredTodoList =
if (showCompleted) toDoList else toDoList.filter { item -> !item.isCompleted }

View file

@ -173,7 +173,7 @@ fun Candle(
) {
// 火苗
Box {
androidx.compose.animation.AnimatedVisibility(
AnimatedVisibility(
visible = showFlame,
enter = scaleIn(),
exit = scaleOut(),

View file

@ -10,25 +10,37 @@ import androidx.compose.material.icons.outlined.ColorLens
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
import cn.super12138.todo.ui.pages.settings.components.SwitchSettingsItem
import cn.super12138.todo.ui.pages.settings.components.appearance.contrast.ContrastPicker
import cn.super12138.todo.ui.pages.settings.components.appearance.darkmode.DarkModePicker
import cn.super12138.todo.ui.pages.settings.components.appearance.palette.PalettePicker
import cn.super12138.todo.ui.viewmodels.MainViewModel
import cn.super12138.todo.ui.theme.ContrastLevel
import cn.super12138.todo.ui.theme.DarkMode
import cn.super12138.todo.ui.theme.PaletteStyle
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsAppearance(
viewModel: MainViewModel,
onNavigateUp: () -> Unit,
modifier: Modifier = Modifier
) {
val dynamicColor by DataStoreManager.dynamicColorFlow.collectAsState(initial = Constants.PREF_DYNAMIC_COLOR_DEFAULT)
val darkMode by DataStoreManager.darkModeFlow.collectAsState(initial = Constants.PREF_DARK_MODE_DEFAULT)
val paletteStyle by DataStoreManager.paletteStyleFlow.collectAsState(initial = Constants.PREF_PALETTE_STYLE_DEFAULT)
val contrastLevel by DataStoreManager.contrastLevelFlow.collectAsState(initial = Constants.PREF_CONTRAST_LEVEL_DEFAULT)
val scope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
LargeTopAppBarScaffold(
title = stringResource(R.string.pref_appearance),
@ -43,23 +55,30 @@ fun SettingsAppearance(
.verticalScroll(rememberScrollState())
) {
SwitchSettingsItem(
key = Constants.PREF_DYNAMIC_COLOR,
default = Constants.PREF_DYNAMIC_COLOR_DEFAULT,
checked = dynamicColor,
leadingIcon = Icons.Outlined.ColorLens,
title = stringResource(R.string.pref_appearance_dynamic_color),
description = stringResource(R.string.pref_appearance_dynamic_color_desc),
onCheckedChange = { viewModel.setDynamicColor(it) },
onCheckedChange = { scope.launch { DataStoreManager.setDynamicColor(it) } },
)
DarkModePicker(onDarkModeChange = { viewModel.setDarkMode(it) })
DarkModePicker(
currentDarkMode = DarkMode.fromId(darkMode),
onDarkModeChange = { scope.launch { DataStoreManager.setDarkMode(it.id) } }
)
PalettePicker(
isDarkMode = viewModel.appDarkMode,
contrastLevel = viewModel.appContrastLevel,
onPaletteChange = { viewModel.setPaletteStyle(it) }
currentPalette = PaletteStyle.fromId(paletteStyle),
onPaletteChange = { scope.launch { DataStoreManager.setPaletteStyle(it.id) } },
isDynamicColor = dynamicColor,
isDarkMode = DarkMode.fromId(darkMode),
contrastLevel = ContrastLevel.fromFloat(contrastLevel)
)
ContrastPicker(onContrastChange = { viewModel.setContrastLevel(it) })
ContrastPicker(
currentContrast = ContrastLevel.fromFloat(contrastLevel),
onContrastChange = { scope.launch { DataStoreManager.setContrastLevel(it.value) } }
)
}
}
}

View file

@ -13,9 +13,11 @@ import androidx.compose.material.icons.outlined.Vibration
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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
@ -24,6 +26,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.logic.model.SortingMethod
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
import cn.super12138.todo.ui.pages.settings.components.SettingsCategory
@ -32,17 +35,21 @@ import cn.super12138.todo.ui.pages.settings.components.SettingsPlainBox
import cn.super12138.todo.ui.pages.settings.components.SettingsRadioDialog
import cn.super12138.todo.ui.pages.settings.components.SettingsRadioOptions
import cn.super12138.todo.ui.pages.settings.components.SwitchSettingsItem
import cn.super12138.todo.ui.viewmodels.MainViewModel
import cn.super12138.todo.utils.VibrationUtils
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsInterface(
viewModel: MainViewModel,
onNavigateUp: () -> Unit,
modifier: Modifier = Modifier
) {
val showCompleted by DataStoreManager.showCompletedFlow.collectAsState(initial = Constants.PREF_SHOW_COMPLETED_DEFAULT)
val secureMode by DataStoreManager.secureModeFlow.collectAsState(initial = Constants.PREF_SECURE_MODE_DEFAULT)
val sortingMethod by DataStoreManager.sortingMethodFlow.collectAsState(initial = Constants.PREF_SORTING_METHOD_DEFAULT)
val hapticFeedback by DataStoreManager.hapticFeedbackFlow.collectAsState(initial = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT)
val context = LocalContext.current
val scope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
var showSortingMethodDialog by rememberSaveable { mutableStateOf(false) }
LargeTopAppBarScaffold(
@ -59,38 +66,34 @@ fun SettingsInterface(
.verticalScroll(rememberScrollState())
) {
SettingsCategory(stringResource(R.string.pref_category_todo_list))
SwitchSettingsItem(
key = Constants.PREF_SHOW_COMPLETED,
default = Constants.PREF_SHOW_COMPLETED_DEFAULT,
checked = showCompleted,
leadingIcon = Icons.Outlined.Checklist,
title = stringResource(R.string.pref_show_completed),
description = stringResource(R.string.pref_show_completed_desc),
onCheckedChange = { viewModel.setShowCompleted(it) },
onCheckedChange = { scope.launch { DataStoreManager.setShowCompleted(it) } },
)
SettingsItem(
leadingIcon = Icons.AutoMirrored.Outlined.Sort,
title = stringResource(R.string.pref_sorting_method),
description = viewModel.appSortingMethod.getDisplayName(context),
description = SortingMethod.fromId(sortingMethod).getDisplayName(context),
onClick = { showSortingMethodDialog = true }
)
SettingsCategory(stringResource(R.string.pref_category_global))
SwitchSettingsItem(
key = Constants.PREF_SECURE_MODE,
default = Constants.PREF_SECURE_MODE_DEFAULT,
checked = secureMode,
leadingIcon = Icons.Outlined.Shield,
title = stringResource(R.string.pref_secure_mode),
description = stringResource(R.string.pref_secure_mode_desc),
onCheckedChange = { viewModel.setSecureMode(it) }
onCheckedChange = { scope.launch { DataStoreManager.setSecureMode(it) } }
)
SwitchSettingsItem(
key = Constants.PREF_HAPTIC_FEEDBACK,
default = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT,
checked = hapticFeedback,
leadingIcon = Icons.Outlined.Vibration,
title = stringResource(R.string.pref_haptic_feedback),
description = stringResource(R.string.pref_haptic_feedback_desc),
onCheckedChange = { VibrationUtils.setEnabled(it) }
onCheckedChange = { scope.launch { DataStoreManager.setHapticFeedback(it) } }
)
SettingsPlainBox(stringResource(R.string.pref_haptic_feedback_more_info))
}
@ -105,14 +108,14 @@ fun SettingsInterface(
}
}
SettingsRadioDialog(
key = Constants.PREF_SORTING_METHOD,
defaultIndex = Constants.PREF_SORTING_METHOD_DEFAULT,
visible = showSortingMethodDialog,
title = stringResource(R.string.pref_sorting_method),
currentOptions = SettingsRadioOptions(
id = sortingMethod,
text = SortingMethod.fromId(sortingMethod).getDisplayName(context)
),
options = sortingList,
onSelect = { id ->
viewModel.setSortingMethod(SortingMethod.fromId(id))
},
onSelect = { scope.launch { DataStoreManager.setSortingMethod(it) } },
onDismiss = { showSortingMethodDialog = false }
)
}

View file

@ -5,14 +5,14 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -21,15 +21,13 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.components.BasicDialog
import cn.super12138.todo.ui.pages.settings.state.rememberPrefIntState
import cn.super12138.todo.utils.VibrationUtils
@Composable
fun SettingsRadioDialog(
key: String,
defaultIndex: Int,
visible: Boolean,
title: String,
currentOptions: SettingsRadioOptions,
options: List<SettingsRadioOptions>,
onSelect: (id: Int) -> Unit,
onDismiss: () -> Unit,
@ -39,15 +37,17 @@ fun SettingsRadioDialog(
visible = visible,
title = title,
text = {
var selectedItemIndex by rememberPrefIntState(key, defaultIndex)
// Modifier.selectableGroup() 用来确保无障碍功能运行正确
Column(Modifier.selectableGroup()) {
Column(
modifier = Modifier
.selectableGroup()
.verticalScroll(rememberScrollState())
) {
options.forEach { option ->
RadioItem(
selected = option.id == selectedItemIndex,
selected = option == currentOptions,
text = option.text,
onClick = {
selectedItemIndex = option.id
onSelect(option.id)
onDismiss()
}
@ -96,11 +96,6 @@ fun RadioItem(
}
}
data class SettingsRadioOptions(
val id: Int,
val text: String,
)
@Composable
fun SettingsDialog(
modifier: Modifier = Modifier,

View file

@ -17,7 +17,6 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.utils.VibrationUtils

View file

@ -0,0 +1,6 @@
package cn.super12138.todo.ui.pages.settings.components
data class SettingsRadioOptions(
val id: Int,
val text: String,
)

View file

@ -3,36 +3,30 @@ package cn.super12138.todo.ui.pages.settings.components
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalView
import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.pages.settings.state.rememberPrefBooleanState
import cn.super12138.todo.utils.VibrationUtils
@Composable
fun SwitchSettingsItem(
modifier: Modifier = Modifier,
key: String,
default: Boolean,
checked: Boolean,
leadingIcon: ImageVector? = null,
title: String,
description: String? = null,
onCheckedChange: (Boolean) -> Unit
) {
val view = LocalView.current
var switchState by rememberPrefBooleanState(key, default)
SettingsItem(
leadingIcon = leadingIcon,
title = title,
description = description,
trailingContent = {
Switch(
checked = switchState,
checked = checked,
onCheckedChange = {
switchState = it
VibrationUtils.performHapticFeedback(view)
onCheckedChange(it)
},
@ -40,8 +34,7 @@ fun SwitchSettingsItem(
)
},
onClick = {
switchState = !switchState
onCheckedChange(switchState)
onCheckedChange(!checked)
},
modifier = modifier
)

View file

@ -1,5 +1,6 @@
package cn.super12138.todo.ui.pages.settings.components.appearance.contrast
import android.util.Log
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@ -13,6 +14,8 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@ -26,14 +29,13 @@ import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.unit.dp
import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.ui.pages.settings.components.MoreContentSettingsItem
import cn.super12138.todo.ui.pages.settings.state.rememberPrefFloatState
import cn.super12138.todo.ui.theme.ContrastLevel
import cn.super12138.todo.utils.VibrationUtils
@Composable
fun ContrastPicker(
currentContrast: ContrastLevel,
onContrastChange: (ContrastLevel) -> Unit,
modifier: Modifier = Modifier
) {
@ -44,27 +46,31 @@ fun ContrastPicker(
description = stringResource(R.string.pref_contrast_level_desc),
modifier = modifier
) {
var contrastState by rememberPrefFloatState(
Constants.PREF_CONTRAST_LEVEL,
Constants.PREF_CONTRAST_LEVEL_DEFAULT
)
val contrastLevelName =
ContrastLevel.entries.map { it.getDisplayName(context) }
var lastVibratedLevel by remember { mutableFloatStateOf(currentContrast.value) }
Slider(
modifier = Modifier.semantics {
contentDescription =
context.getString(R.string.pref_contrast_level) + contrastLevelName[ContrastLevel.fromFloat(
contrastState
).ordinal]
stateDescription = contrastLevelName[ContrastLevel.fromFloat(contrastState).ordinal]
context.getString(R.string.pref_contrast_level) + contrastLevelName[currentContrast.ordinal]
stateDescription = contrastLevelName[currentContrast.ordinal]
liveRegion = LiveRegionMode.Polite
},
value = contrastState,
onValueChange = {
VibrationUtils.performHapticFeedback(view, HapticFeedbackConstants.LONG_PRESS)
contrastState = it
onContrastChange(ContrastLevel.fromFloat(it))
value = currentContrast.value,
onValueChange = { newValue ->
// 更新状态
onContrastChange(ContrastLevel.fromFloat(newValue))
// 只有当分段值变化时才触发震动
if (newValue != lastVibratedLevel) {
VibrationUtils.performHapticFeedback(
view,
HapticFeedbackConstants.LONG_PRESS
)
Log.d("ContrastPicker", "Level changed to: ${newValue}")
lastVibratedLevel = newValue
}
},
valueRange = -1f..1f,
steps = 3,

View file

@ -27,7 +27,7 @@ import cn.super12138.todo.utils.VibrationUtils
@Composable
fun DarkModeItem(
icon: ImageVector,
contentDescription: String,
name: String,
contentColor: Color,
containerColor: Color,
selected: Boolean,
@ -73,7 +73,7 @@ fun DarkModeItem(
Spacer(Modifier.size(8.dp))
Text(
text = contentDescription,
text = name,
style = MaterialTheme.typography.bodyMedium
)
}

View file

@ -3,32 +3,23 @@ package cn.super12138.todo.ui.pages.settings.components.appearance.darkmode
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.ui.pages.settings.components.RowSettingsItem
import cn.super12138.todo.ui.pages.settings.state.rememberPrefIntState
import cn.super12138.todo.ui.theme.DarkMode
@Composable
fun DarkModePicker(
currentDarkMode: DarkMode,
onDarkModeChange: (darkMode: DarkMode) -> Unit,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val isInDarkTheme = isSystemInDarkTheme()
var darkModeState by rememberPrefIntState(
Constants.PREF_DARK_MODE,
Constants.PREF_DARK_MODE_DEFAULT
)
RowSettingsItem(
title = stringResource(R.string.pref_dark_mode),
description = stringResource(R.string.pref_dark_mode_desc),
@ -38,38 +29,29 @@ fun DarkModePicker(
) {
DarkModeItem(
icon = DarkMode.FollowSystem.icon,
contentDescription = DarkMode.FollowSystem.getDisplayName(context),
name = DarkMode.FollowSystem.getDisplayName(context),
contentColor = if (isInDarkTheme) Color.White else Color.Black,
containerColor = if (isInDarkTheme) Color.Black else Color.White,
selected = DarkMode.fromId(darkModeState) == DarkMode.FollowSystem,
onSelect = {
darkModeState = DarkMode.FollowSystem.id
onDarkModeChange(DarkMode.FollowSystem)
}
selected = currentDarkMode == DarkMode.FollowSystem,
onSelect = { onDarkModeChange(DarkMode.FollowSystem) }
)
DarkModeItem(
icon = DarkMode.Light.icon,
contentDescription = DarkMode.Light.getDisplayName(context),
name = DarkMode.Light.getDisplayName(context),
contentColor = Color.Black,
containerColor = Color.White,
selected = DarkMode.fromId(darkModeState) == DarkMode.Light,
onSelect = {
darkModeState = DarkMode.Light.id
onDarkModeChange(DarkMode.Light)
}
selected = currentDarkMode == DarkMode.Light,
onSelect = { onDarkModeChange(DarkMode.Light) }
)
DarkModeItem(
icon = DarkMode.Dark.icon,
contentDescription = DarkMode.Dark.getDisplayName(context),
name = DarkMode.Dark.getDisplayName(context),
contentColor = Color.White,
containerColor = Color.Black,
selected = DarkMode.fromId(darkModeState) == DarkMode.Dark,
onSelect = {
darkModeState = DarkMode.Dark.id
onDarkModeChange(DarkMode.Dark)
}
selected = currentDarkMode == DarkMode.Dark,
onSelect = { onDarkModeChange(DarkMode.Dark) }
)
}
}

View file

@ -28,7 +28,6 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import cn.super12138.todo.constants.GlobalValues
import cn.super12138.todo.ui.theme.ContrastLevel
import cn.super12138.todo.ui.theme.PaletteStyle
import cn.super12138.todo.ui.theme.dynamicColorScheme
@ -36,6 +35,7 @@ import cn.super12138.todo.utils.VibrationUtils
@Composable
fun PaletteItem(
isDynamicColor: Boolean,
isDark: Boolean,
paletteStyle: PaletteStyle,
contrastLevel: ContrastLevel,
@ -63,7 +63,7 @@ fun PaletteItem(
// 为不同主题样式设置不同色板
MaterialTheme(
colorScheme = dynamicColorScheme(
keyColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && GlobalValues.dynamicColor) {
keyColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isDynamicColor) {
colorResource(id = android.R.color.system_accent1_500)
} else {
Color(0xFF0061A4)

View file

@ -3,31 +3,25 @@ package cn.super12138.todo.ui.pages.settings.components.appearance.palette
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants
import cn.super12138.todo.ui.pages.settings.components.RowSettingsItem
import cn.super12138.todo.ui.pages.settings.state.rememberPrefIntState
import cn.super12138.todo.ui.theme.ContrastLevel
import cn.super12138.todo.ui.theme.DarkMode
import cn.super12138.todo.ui.theme.PaletteStyle
@Composable
fun PalettePicker(
currentPalette: PaletteStyle,
onPaletteChange: (paletteStyle: PaletteStyle) -> Unit,
isDynamicColor: Boolean,
isDarkMode: DarkMode,
contrastLevel: ContrastLevel,
onPaletteChange: (paletteStyle: PaletteStyle) -> Unit,
modifier: Modifier = Modifier
) {
var paletteState by rememberPrefIntState(
Constants.PREF_PALETTE_STYLE,
Constants.PREF_PALETTE_STYLE_DEFAULT
)
val paletteOptions = remember {
PaletteStyle.entries.toList()
@ -42,18 +36,16 @@ fun PalettePicker(
) {
paletteOptions.forEach { paletteStyle ->
PaletteItem(
isDynamicColor = isDynamicColor,
isDark = when (isDarkMode) {
DarkMode.FollowSystem -> isSystemInDarkTheme()
DarkMode.Light -> false
DarkMode.Dark -> true
},
paletteStyle = paletteStyle,
selected = PaletteStyle.fromId(paletteState) == paletteStyle,
selected = currentPalette == paletteStyle,
contrastLevel = contrastLevel,
onSelect = {
paletteState = paletteStyle.id
onPaletteChange(paletteStyle)
}
onSelect = { onPaletteChange(paletteStyle) }
)
}
}

View file

@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.Typography
@ -98,7 +97,7 @@ fun LicenceItem(
modifier = Modifier.padding(padding.licensePadding.contentPadding),
maxLines = 1,
text = it.name,
style = MaterialTheme.typography.labelSmall,
// style = MaterialTheme.typography.labelSmall,
textAlign = TextAlign.Center,
overflow = LibraryDefaults.libraryTextStyles().defaultOverflow,
)

View file

@ -1,119 +0,0 @@
package cn.super12138.todo.ui.pages.settings.state
import android.content.Context
import android.content.SharedPreferences
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableFloatState
import androidx.compose.runtime.MutableIntState
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.edit
/**
* 来自https://github.com/hushenghao/AndroidEasterEggs/blob/main/app/src/main/java/com/dede/android_eggs/views/settings/compose/basic/PrefMutableState.kt
* 在命名上有改动
* @author hushenghao
*/
@Composable
fun rememberPrefBooleanState(key: String, default: Boolean): MutableState<Boolean> {
val context = LocalContext.current
return remember { PrefMutableBooleanState(context, key, default) }
}
@Composable
fun rememberPrefIntState(key: String, default: Int): MutableIntState {
val context = LocalContext.current
return remember { PrefMutableIntState(context, key, default) }
}
@Composable
fun rememberPrefFloatState(key: String, default: Float): MutableFloatState {
val context = LocalContext.current
return remember { PrefMutableFloatState(context, key, default) }
}
private class PrefMutableBooleanState(
val context: Context,
val key: String,
default: Boolean,
) : MutableState<Boolean> {
private val delegate = mutableStateOf(context.pref.getBoolean(key, default))
override var value: Boolean
get() = delegate.value
set(value) {
delegate.value = value
context.pref.edit { putBoolean(key, value) }
}
override fun component1(): Boolean {
return delegate.component1()
}
override fun component2(): (Boolean) -> Unit {
return delegate.component2()
}
}
private class PrefMutableIntState(
val context: Context,
val key: String,
default: Int,
) : MutableIntState {
private val delegate = mutableIntStateOf(context.pref.getInt(key, default))
override var intValue: Int
get() = delegate.intValue
set(value) {
delegate.intValue = value
context.pref.edit { putInt(key, value) }
}
override fun component1(): Int {
return delegate.component1()
}
override fun component2(): (Int) -> Unit {
return delegate.component2()
}
}
private class PrefMutableFloatState(
val context: Context,
val key: String,
default: Float,
) : MutableFloatState {
private val delegate = mutableFloatStateOf(context.pref.getFloat(key, default))
override var floatValue: Float
get() = delegate.floatValue
set(value) {
delegate.floatValue = value
context.pref.edit { putFloat(key, value) }
}
override fun component1(): Float {
return delegate.component1()
}
override fun component2(): (Float) -> Unit {
return delegate.component2()
}
}
/**
* 来自https://github.com/hushenghao/AndroidEasterEggs/blob/main/basic/src/main/java/com/dede/android_eggs/util/Pref.kt
* @author hushenghao
*/
val Context.pref: SharedPreferences
get() {
return applicationContext.getSharedPreferences(
applicationContext.packageName + "_preferences",
Context.MODE_PRIVATE
)
}

View file

@ -9,19 +9,18 @@ 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.datastore.DataStoreManager
import cn.super12138.todo.logic.model.SortingMethod
import cn.super12138.todo.ui.theme.ContrastLevel
import cn.super12138.todo.ui.theme.DarkMode
import cn.super12138.todo.ui.theme.PaletteStyle
import cn.super12138.todo.utils.FileUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@ -33,37 +32,27 @@ import java.io.FileOutputStream
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModel : ViewModel() {
// 待办
private val toDos: Flow<List<TodoEntity>> = Repository.getAllTodos()
var appSortingMethod by mutableStateOf(SortingMethod.fromId(GlobalValues.sortingMethod))
val sortedTodos: Flow<List<TodoEntity>> = toDos.map { list ->
when (appSortingMethod) {
SortingMethod.Sequential -> list.sortedBy { it.id }
SortingMethod.Subject -> list.sortedBy { it.subject }
SortingMethod.Priority -> list.sortedByDescending { it.priority } // 优先级高的在前
SortingMethod.Completion -> list.sortedBy { it.isCompleted } // 未完成的在前
SortingMethod.AlphabeticalAscending -> list.sortedBy { it.content }
SortingMethod.AlphabeticalDescending -> list.sortedByDescending { it.content }
val sortedTodos: Flow<List<TodoEntity>> =
DataStoreManager.sortingMethodFlow.flatMapLatest { sortingMethod ->
toDos.map { list ->
when (SortingMethod.fromId(sortingMethod)) {
SortingMethod.Sequential -> list.sortedBy { it.id }
SortingMethod.Subject -> list.sortedBy { it.subject }
SortingMethod.Priority -> list.sortedByDescending { it.priority } // 优先级高的在前
SortingMethod.Completion -> list.sortedBy { it.isCompleted } // 未完成的在前
SortingMethod.AlphabeticalAscending -> list.sortedBy { it.content }
SortingMethod.AlphabeticalDescending -> list.sortedByDescending { it.content }
}
}
}
}
val showConfetti = mutableStateOf(false)
var selectedEditTodo by mutableStateOf<TodoEntity?>(null)
private set
var showCompletedTodos by mutableStateOf(GlobalValues.showCompleted)
// 主题颜色
var appDynamicColorEnable by mutableStateOf(GlobalValues.dynamicColor)
private set
var appPaletteStyle by mutableStateOf(PaletteStyle.fromId(GlobalValues.paletteStyle))
private set
var appDarkMode by mutableStateOf(DarkMode.fromId(GlobalValues.darkMode))
private set
var appContrastLevel by mutableStateOf(ContrastLevel.fromFloat(GlobalValues.contrastLevel))
private set
var appSecureMode by mutableStateOf(GlobalValues.secureMode)
private set
// 多选逻辑参考https://github.com/X1nto/Mauth
private val _selectedTodoIds = MutableStateFlow(listOf<Int>())
@ -147,36 +136,12 @@ class MainViewModel : ViewModel() {
}
/**
* 应用设置
* 备份应用数据
*
* @param uri 备份文件路径的 URI
* @param context 应用 Context
* @param onResult 备份完成的回调函数
*/
fun setDynamicColor(enabled: Boolean) {
appDynamicColorEnable = enabled
}
fun setPaletteStyle(paletteStyle: PaletteStyle) {
appPaletteStyle = paletteStyle
}
fun setDarkMode(darkMode: DarkMode) {
appDarkMode = darkMode
}
fun setContrastLevel(contrastLevel: ContrastLevel) {
appContrastLevel = contrastLevel
}
fun setShowCompleted(show: Boolean) {
showCompletedTodos = show
}
fun setSortingMethod(sortingMethod: SortingMethod) {
appSortingMethod = sortingMethod
}
fun setSecureMode(enabled: Boolean) {
appSecureMode = enabled
}
fun backupAppData(uri: Uri, context: Context, onResult: (completed: Boolean) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val result = runCatching {
@ -192,6 +157,13 @@ class MainViewModel : ViewModel() {
}
}
/**
* 恢复应用数据
*
* @param uri 选择的恢复文件的 URI
* @param context 应用 Context
* @param onResult 恢复完成的回调函数
*/
fun restoreAppData(uri: Uri, context: Context, onResult: (completed: Boolean) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
val result = runCatching {
@ -205,23 +177,36 @@ class MainViewModel : ViewModel() {
}
}
/**
* 获取要备份文件的文件列表
* * 数据库文件
* * 数据库的 wal 文件
* * 数据库的 shm 文件
* * DataStore Preferences 文件
*/
private fun getBackupFiles(context: Context): List<File> {
val dbPath = TodoApp.db.openHelper.writableDatabase.path
val prefPath = "${context.filesDir.parent}/shared_prefs"
val prefPath = "${context.filesDir}/datastore"
return listOf(
context.getDatabasePath(Constants.DB_NAME), // 数据库
File("$dbPath-wal"), // 数据库-wal
File("$dbPath-shm"), // 数据库-shm
File("$prefPath/${Constants.SP_NAME}.xml") // SharedPreferences
File("$prefPath/${Constants.SP_NAME}.preferences_pb") // DataStore Preferences
).filter { it.exists() }
}
/**
* 解压 zip 备份文件
*
* @param zipInputStream 备份文件中每个文件的输入流
* @param context 应用 Context
*/
private fun extractZipEntries(zipInputStream: ZipInputStream, context: Context) {
val dbPath = context.getDatabasePath(Constants.DB_NAME).parent
val prefPath = "${context.filesDir.parent}/shared_prefs/"
val prefPath = "${context.filesDir}/datastore/"
generateSequence { zipInputStream.nextEntry }.forEach { zipEntry ->
val outputFile = File(
if (zipEntry.name.endsWith(".xml")) prefPath else dbPath,
if (zipEntry.name.endsWith(".preferences_pb")) prefPath else dbPath,
zipEntry.name
)
if (zipEntry.isDirectory) {

View file

@ -1,14 +0,0 @@
package cn.super12138.todo.utils
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class SPDelegates<T>(private val key: String, private val default: T) : ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return SPUtils.getValue(key, default)
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
SPUtils.putValue(key, value)
}
}

View file

@ -2,10 +2,10 @@ package cn.super12138.todo.utils
import android.view.HapticFeedbackConstants
import android.view.View
import cn.super12138.todo.constants.GlobalValues
import cn.super12138.todo.constants.Constants
object VibrationUtils {
private var isEnabled: Boolean = GlobalValues.hapticFeedback
private var isEnabled: Boolean = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT
fun setEnabled(enabled: Boolean) {
isEnabled = enabled

View file

@ -3,7 +3,7 @@
coreKtx = "1.16.0"
splashScreen = "1.2.0-beta02"
lifecycleRuntimeKtx = "2.9.1"
# security = "1.1.0-alpha06"
datastore = "1.1.7"
# Compose
activityCompose = "1.10.1"
composeBom = "2025.06.01"
@ -41,7 +41,7 @@ androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecyc
androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleRuntimeKtx" }
androidx-compose-runtime-livedata = { group = "androidx.compose.runtime", name = "runtime-livedata", version.ref = "liveData" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
# androidx-security = { group = "androidx.security", name = "security-crypto", version.ref = "security" }
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
# Compose
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }