From d7eec36fc8c83664db7306fc1b98af3d14d14f82 Mon Sep 17 00:00:00 2001 From: Super12138 <70494801+Super12138@users.noreply.github.com> Date: Fri, 27 Jun 2025 14:57:29 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=94=A8=20Preference=20DataStore?= =?UTF-8?q?=20=E4=BB=A3=E6=9B=BF=20Preference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 1 + .../super12138/todo/constants/GlobalValues.kt | 38 ------ .../todo/logic/datastore/DataStoreManager.kt | 124 ++++++++++++++++++ .../todo/ui/activities/CrashActivity.kt | 34 +++-- .../todo/ui/activities/MainActivity.kt | 43 ++++-- .../todo/ui/navigation/TodoNavigation.kt | 14 +- .../super12138/todo/ui/pages/main/MainPage.kt | 4 +- .../ui/pages/settings/SettingsAboutSpecial.kt | 2 +- .../ui/pages/settings/SettingsAppearance.kt | 39 ++++-- .../settings/SettingsInterfaceInteraction.kt | 41 +++--- .../settings/components/SettingsDialogs.kt | 23 ++-- .../pages/settings/components/SettingsItem.kt | 1 - .../components/SettingsRadioOptions.kt | 6 + .../settings/components/SwitchSettingsItem.kt | 13 +- .../appearance/contrast/ContrastPicker.kt | 36 ++--- .../appearance/darkmode/DarkModeItem.kt | 4 +- .../appearance/darkmode/DarkModePicker.kt | 38 ++---- .../appearance/palette/PaletteItem.kt | 4 +- .../appearance/palette/PalettePicker.kt | 20 +-- .../components/licence/LicenceItem.kt | 3 +- .../pages/settings/state/PrefMutableState.kt | 119 ----------------- .../todo/ui/viewmodels/MainViewModel.kt | 105 +++++++-------- .../cn/super12138/todo/utils/SPDelegates.kt | 14 -- .../super12138/todo/utils/VibrationUtils.kt | 4 +- gradle/libs.versions.toml | 4 +- 25 files changed, 344 insertions(+), 390 deletions(-) delete mode 100644 app/src/main/kotlin/cn/super12138/todo/constants/GlobalValues.kt create mode 100644 app/src/main/kotlin/cn/super12138/todo/logic/datastore/DataStoreManager.kt create mode 100644 app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsRadioOptions.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/state/PrefMutableState.kt delete mode 100644 app/src/main/kotlin/cn/super12138/todo/utils/SPDelegates.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 562d0da..f952d28 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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)) diff --git a/app/src/main/kotlin/cn/super12138/todo/constants/GlobalValues.kt b/app/src/main/kotlin/cn/super12138/todo/constants/GlobalValues.kt deleted file mode 100644 index 53c5fb6..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/constants/GlobalValues.kt +++ /dev/null @@ -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 - ) -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/datastore/DataStoreManager.kt b/app/src/main/kotlin/cn/super12138/todo/logic/datastore/DataStoreManager.kt new file mode 100644 index 0000000..27be618 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/logic/datastore/DataStoreManager.kt @@ -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 = 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 = dataStore.data.map { preferences -> + preferences[SHOW_COMPLETED] ?: Constants.PREF_SHOW_COMPLETED_DEFAULT + } + + val sortingMethodFlow: Flow = dataStore.data.map { preferences -> + preferences[SORTING_METHOD] ?: Constants.PREF_SORTING_METHOD_DEFAULT + } + + val secureModeFlow: Flow = dataStore.data.map { preferences -> + preferences[SECURE_MODE] ?: Constants.PREF_SECURE_MODE_DEFAULT + } + + val hapticFeedbackFlow: Flow = 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt b/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt index 942efd6..11724d2 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/activities/CrashActivity.kt @@ -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), diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/activities/MainActivity.kt b/app/src/main/kotlin/cn/super12138/todo/ui/activities/MainActivity.kt index db98f7a..a31cf2a 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/activities/MainActivity.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/activities/MainActivity.kt @@ -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( diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt index 12d5529..5524cdf 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoNavigation.kt @@ -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() }) } } } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/MainPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/MainPage.kt index c0c3493..d7ba026 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/MainPage.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/main/MainPage.kt @@ -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 } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAboutSpecial.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAboutSpecial.kt index fb46426..3f205ae 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAboutSpecial.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAboutSpecial.kt @@ -173,7 +173,7 @@ fun Candle( ) { // 火苗 Box { - androidx.compose.animation.AnimatedVisibility( + AnimatedVisibility( visible = showFlame, enter = scaleIn(), exit = scaleOut(), diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAppearance.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAppearance.kt index 0730f59..d288ad5 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAppearance.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsAppearance.kt @@ -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) } } + ) } } } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsInterfaceInteraction.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsInterfaceInteraction.kt index 72722cd..f99f329 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsInterfaceInteraction.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsInterfaceInteraction.kt @@ -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 } ) } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsDialogs.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsDialogs.kt index 24d8f04..1dcd78e 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsDialogs.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsDialogs.kt @@ -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, 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, diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt index e13839e..c4f4c35 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsItem.kt @@ -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 diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsRadioOptions.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsRadioOptions.kt new file mode 100644 index 0000000..dfa0e52 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SettingsRadioOptions.kt @@ -0,0 +1,6 @@ +package cn.super12138.todo.ui.pages.settings.components + +data class SettingsRadioOptions( + val id: Int, + val text: String, +) \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SwitchSettingsItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SwitchSettingsItem.kt index 21d5243..558b9b6 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SwitchSettingsItem.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/SwitchSettingsItem.kt @@ -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 ) diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/contrast/ContrastPicker.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/contrast/ContrastPicker.kt index cf5885d..ef9a0e9 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/contrast/ContrastPicker.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/contrast/ContrastPicker.kt @@ -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, diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModeItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModeItem.kt index bd1d8da..53b7843 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModeItem.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModeItem.kt @@ -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 ) } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModePicker.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModePicker.kt index 1dab847..b738110 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModePicker.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/darkmode/DarkModePicker.kt @@ -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) } ) } } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PaletteItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PaletteItem.kt index 3068a3e..c95c2ca 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PaletteItem.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PaletteItem.kt @@ -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) diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PalettePicker.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PalettePicker.kt index f021705..8fbe0f1 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PalettePicker.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/appearance/palette/PalettePicker.kt @@ -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) } ) } } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/licence/LicenceItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/licence/LicenceItem.kt index edd9c20..d699c21 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/licence/LicenceItem.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/licence/LicenceItem.kt @@ -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, ) diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/state/PrefMutableState.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/state/PrefMutableState.kt deleted file mode 100644 index 174d350..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/state/PrefMutableState.kt +++ /dev/null @@ -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 { - 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 { - 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 - ) - } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt index 159d680..987988e 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt @@ -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> = Repository.getAllTodos() - var appSortingMethod by mutableStateOf(SortingMethod.fromId(GlobalValues.sortingMethod)) - val sortedTodos: Flow> = 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> = + 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(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()) @@ -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 { 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) { diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/SPDelegates.kt b/app/src/main/kotlin/cn/super12138/todo/utils/SPDelegates.kt deleted file mode 100644 index 5af97cd..0000000 --- a/app/src/main/kotlin/cn/super12138/todo/utils/SPDelegates.kt +++ /dev/null @@ -1,14 +0,0 @@ -package cn.super12138.todo.utils - -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - -class SPDelegates(private val key: String, private val default: T) : ReadWriteProperty { - 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) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt b/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt index 6c8e0a5..2c5cb30 100644 --- a/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt +++ b/app/src/main/kotlin/cn/super12138/todo/utils/VibrationUtils.kt @@ -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 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fc29900..c18df6e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }