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.runtime.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.compose.runtime.livedata) implementation(libs.androidx.compose.runtime.livedata)
implementation(libs.androidx.datastore.preferences)
// Compose // Compose
implementation(libs.androidx.activity.compose) implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom)) 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.isSystemInDarkTheme
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import cn.super12138.todo.R 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.pages.crash.CrashPage
import cn.super12138.todo.ui.theme.DarkMode 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.PaletteStyle
import cn.super12138.todo.ui.theme.ToDoTheme import cn.super12138.todo.ui.theme.ToDoTheme
import cn.super12138.todo.utils.VibrationUtils
class CrashActivity : ComponentActivity() { class CrashActivity : ComponentActivity() {
companion object { companion object {
@ -40,11 +41,16 @@ class CrashActivity : ComponentActivity() {
val crashLogs = intent.getStringExtra("crash_logs") val crashLogs = intent.getStringExtra("crash_logs")
setContent { setContent {
val darkMode = DarkMode.fromId(GlobalValues.darkMode) val dynamicColor by DataStoreManager.dynamicColorFlow.collectAsState(initial = Constants.PREF_DYNAMIC_COLOR_DEFAULT)
val darkTheme = when (darkMode) { val paletteStyle by DataStoreManager.paletteStyleFlow.collectAsState(initial = Constants.PREF_PALETTE_STYLE_DEFAULT)
FollowSystem -> isSystemInDarkTheme() val contrastLevel by DataStoreManager.contrastLevelFlow.collectAsState(initial = Constants.PREF_CONTRAST_LEVEL_DEFAULT)
Light -> false val darkMode by DataStoreManager.darkModeFlow.collectAsState(initial = Constants.PREF_DARK_MODE_DEFAULT)
Dark -> true 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 // 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( ToDoTheme(
darkTheme = darkTheme, darkTheme = darkTheme,
style = PaletteStyle.fromId(GlobalValues.paletteStyle), style = PaletteStyle.fromId(paletteStyle),
contrastLevel = GlobalValues.contrastLevel.toDouble(), contrastLevel = contrastLevel.toDouble(),
dynamicColor = GlobalValues.dynamicColor dynamicColor = dynamicColor
) { ) {
CrashPage( CrashPage(
crashLog = crashLogs ?: stringResource(R.string.tip_no_crash_logs), 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.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.viewmodel.compose.viewModel 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.components.Konfetti
import cn.super12138.todo.ui.navigation.TodoNavigation import cn.super12138.todo.ui.navigation.TodoNavigation
import cn.super12138.todo.ui.theme.DarkMode.Dark import cn.super12138.todo.ui.theme.DarkMode
import cn.super12138.todo.ui.theme.DarkMode.FollowSystem import cn.super12138.todo.ui.theme.PaletteStyle
import cn.super12138.todo.ui.theme.DarkMode.Light
import cn.super12138.todo.ui.theme.ToDoTheme import cn.super12138.todo.ui.theme.ToDoTheme
import cn.super12138.todo.ui.viewmodels.MainViewModel import cn.super12138.todo.ui.viewmodels.MainViewModel
import cn.super12138.todo.utils.VibrationUtils
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -34,23 +38,32 @@ class MainActivity : ComponentActivity() {
setContent { setContent {
val mainViewModel: MainViewModel = viewModel() val mainViewModel: MainViewModel = viewModel()
val showConfetti = mainViewModel.showConfetti 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) { val darkTheme = when (DarkMode.fromId(darkMode)) {
FollowSystem -> isSystemInDarkTheme() DarkMode.FollowSystem -> isSystemInDarkTheme()
Light -> false DarkMode.Light -> false
Dark -> true DarkMode.Dark -> true
} }
// 配置状态栏和底部导航栏的颜色(在用户切换深色模式时) // 配置状态栏和底部导航栏的颜色(在用户切换深色模式时)
// https://github.com/dn0ne/lotus/blob/master/app/src/main/java/com/dn0ne/player/MainActivity.kt#L266 // 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 { WindowCompat.getInsetsController(window, window.decorView).apply {
isAppearanceLightStatusBars = !darkTheme isAppearanceLightStatusBars = !darkTheme
isAppearanceLightNavigationBars = !darkTheme isAppearanceLightNavigationBars = !darkTheme
} }
} }
// 阻止截屏相关配置 // 阻止截屏相关配置
LaunchedEffect(mainViewModel.appSecureMode) { LaunchedEffect(secureMode) {
if (mainViewModel.appSecureMode) { if (secureMode) {
window.setFlags( window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE WindowManager.LayoutParams.FLAG_SECURE
@ -60,11 +73,15 @@ class MainActivity : ComponentActivity() {
} }
} }
LaunchedEffect(hapticFeedback) {
VibrationUtils.setEnabled(hapticFeedback)
}
ToDoTheme( ToDoTheme(
darkTheme = darkTheme, darkTheme = darkTheme,
style = mainViewModel.appPaletteStyle, style = PaletteStyle.fromId(paletteStyle),
contrastLevel = mainViewModel.appContrastLevel.value.toDouble(), contrastLevel = contrastLevel.toDouble(),
dynamicColor = mainViewModel.appDynamicColorEnable dynamicColor = dynamicColor
) { ) {
Surface(color = MaterialTheme.colorScheme.background) { Surface(color = MaterialTheme.colorScheme.background) {
TodoNavigation( TodoNavigation(

View file

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

View file

@ -33,6 +33,7 @@ import androidx.window.core.layout.WindowWidthSizeClass
import cn.super12138.todo.R import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.database.TodoEntity 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.AnimatedExtendedFloatingActionButton
import cn.super12138.todo.ui.components.ConfirmDialog import cn.super12138.todo.ui.components.ConfirmDialog
import cn.super12138.todo.ui.pages.main.components.TodoTopAppBar import cn.super12138.todo.ui.pages.main.components.TodoTopAppBar
@ -72,7 +73,8 @@ fun MainPage(
val toDoList by remember { derivedStateOf { toDos.value } } 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 = val filteredTodoList =
if (showCompleted) toDoList else toDoList.filter { item -> !item.isCompleted } if (showCompleted) toDoList else toDoList.filter { item -> !item.isCompleted }

View file

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

View file

@ -10,25 +10,37 @@ import androidx.compose.material.icons.outlined.ColorLens
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import cn.super12138.todo.R import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants 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.components.LargeTopAppBarScaffold
import cn.super12138.todo.ui.pages.settings.components.SwitchSettingsItem 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.contrast.ContrastPicker
import cn.super12138.todo.ui.pages.settings.components.appearance.darkmode.DarkModePicker 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.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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsAppearance( fun SettingsAppearance(
viewModel: MainViewModel,
onNavigateUp: () -> Unit, onNavigateUp: () -> Unit,
modifier: Modifier = Modifier 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() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
LargeTopAppBarScaffold( LargeTopAppBarScaffold(
title = stringResource(R.string.pref_appearance), title = stringResource(R.string.pref_appearance),
@ -43,23 +55,30 @@ fun SettingsAppearance(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
SwitchSettingsItem( SwitchSettingsItem(
key = Constants.PREF_DYNAMIC_COLOR, checked = dynamicColor,
default = Constants.PREF_DYNAMIC_COLOR_DEFAULT,
leadingIcon = Icons.Outlined.ColorLens, leadingIcon = Icons.Outlined.ColorLens,
title = stringResource(R.string.pref_appearance_dynamic_color), title = stringResource(R.string.pref_appearance_dynamic_color),
description = stringResource(R.string.pref_appearance_dynamic_color_desc), 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( PalettePicker(
isDarkMode = viewModel.appDarkMode, currentPalette = PaletteStyle.fromId(paletteStyle),
contrastLevel = viewModel.appContrastLevel, onPaletteChange = { scope.launch { DataStoreManager.setPaletteStyle(it.id) } },
onPaletteChange = { viewModel.setPaletteStyle(it) } 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.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -24,6 +26,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import cn.super12138.todo.R import cn.super12138.todo.R
import cn.super12138.todo.constants.Constants import cn.super12138.todo.constants.Constants
import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.logic.model.SortingMethod import cn.super12138.todo.logic.model.SortingMethod
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
import cn.super12138.todo.ui.pages.settings.components.SettingsCategory 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.SettingsRadioDialog
import cn.super12138.todo.ui.pages.settings.components.SettingsRadioOptions import cn.super12138.todo.ui.pages.settings.components.SettingsRadioOptions
import cn.super12138.todo.ui.pages.settings.components.SwitchSettingsItem import cn.super12138.todo.ui.pages.settings.components.SwitchSettingsItem
import cn.super12138.todo.ui.viewmodels.MainViewModel import kotlinx.coroutines.launch
import cn.super12138.todo.utils.VibrationUtils
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsInterface( fun SettingsInterface(
viewModel: MainViewModel,
onNavigateUp: () -> Unit, onNavigateUp: () -> Unit,
modifier: Modifier = Modifier 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 context = LocalContext.current
val scope = rememberCoroutineScope()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
var showSortingMethodDialog by rememberSaveable { mutableStateOf(false) } var showSortingMethodDialog by rememberSaveable { mutableStateOf(false) }
LargeTopAppBarScaffold( LargeTopAppBarScaffold(
@ -59,38 +66,34 @@ fun SettingsInterface(
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
SettingsCategory(stringResource(R.string.pref_category_todo_list)) SettingsCategory(stringResource(R.string.pref_category_todo_list))
SwitchSettingsItem( SwitchSettingsItem(
key = Constants.PREF_SHOW_COMPLETED, checked = showCompleted,
default = Constants.PREF_SHOW_COMPLETED_DEFAULT,
leadingIcon = Icons.Outlined.Checklist, leadingIcon = Icons.Outlined.Checklist,
title = stringResource(R.string.pref_show_completed), title = stringResource(R.string.pref_show_completed),
description = stringResource(R.string.pref_show_completed_desc), description = stringResource(R.string.pref_show_completed_desc),
onCheckedChange = { viewModel.setShowCompleted(it) }, onCheckedChange = { scope.launch { DataStoreManager.setShowCompleted(it) } },
) )
SettingsItem( SettingsItem(
leadingIcon = Icons.AutoMirrored.Outlined.Sort, leadingIcon = Icons.AutoMirrored.Outlined.Sort,
title = stringResource(R.string.pref_sorting_method), title = stringResource(R.string.pref_sorting_method),
description = viewModel.appSortingMethod.getDisplayName(context), description = SortingMethod.fromId(sortingMethod).getDisplayName(context),
onClick = { showSortingMethodDialog = true } onClick = { showSortingMethodDialog = true }
) )
SettingsCategory(stringResource(R.string.pref_category_global)) SettingsCategory(stringResource(R.string.pref_category_global))
SwitchSettingsItem( SwitchSettingsItem(
key = Constants.PREF_SECURE_MODE, checked = secureMode,
default = Constants.PREF_SECURE_MODE_DEFAULT,
leadingIcon = Icons.Outlined.Shield, leadingIcon = Icons.Outlined.Shield,
title = stringResource(R.string.pref_secure_mode), title = stringResource(R.string.pref_secure_mode),
description = stringResource(R.string.pref_secure_mode_desc), description = stringResource(R.string.pref_secure_mode_desc),
onCheckedChange = { viewModel.setSecureMode(it) } onCheckedChange = { scope.launch { DataStoreManager.setSecureMode(it) } }
) )
SwitchSettingsItem( SwitchSettingsItem(
key = Constants.PREF_HAPTIC_FEEDBACK, checked = hapticFeedback,
default = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT,
leadingIcon = Icons.Outlined.Vibration, leadingIcon = Icons.Outlined.Vibration,
title = stringResource(R.string.pref_haptic_feedback), title = stringResource(R.string.pref_haptic_feedback),
description = stringResource(R.string.pref_haptic_feedback_desc), 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)) SettingsPlainBox(stringResource(R.string.pref_haptic_feedback_more_info))
} }
@ -105,14 +108,14 @@ fun SettingsInterface(
} }
} }
SettingsRadioDialog( SettingsRadioDialog(
key = Constants.PREF_SORTING_METHOD,
defaultIndex = Constants.PREF_SORTING_METHOD_DEFAULT,
visible = showSortingMethodDialog, visible = showSortingMethodDialog,
title = stringResource(R.string.pref_sorting_method), title = stringResource(R.string.pref_sorting_method),
currentOptions = SettingsRadioOptions(
id = sortingMethod,
text = SortingMethod.fromId(sortingMethod).getDisplayName(context)
),
options = sortingList, options = sortingList,
onSelect = { id -> onSelect = { scope.launch { DataStoreManager.setSortingMethod(it) } },
viewModel.setSortingMethod(SortingMethod.fromId(id))
},
onDismiss = { showSortingMethodDialog = false } 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.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -21,15 +21,13 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.components.BasicDialog import cn.super12138.todo.ui.components.BasicDialog
import cn.super12138.todo.ui.pages.settings.state.rememberPrefIntState
import cn.super12138.todo.utils.VibrationUtils import cn.super12138.todo.utils.VibrationUtils
@Composable @Composable
fun SettingsRadioDialog( fun SettingsRadioDialog(
key: String,
defaultIndex: Int,
visible: Boolean, visible: Boolean,
title: String, title: String,
currentOptions: SettingsRadioOptions,
options: List<SettingsRadioOptions>, options: List<SettingsRadioOptions>,
onSelect: (id: Int) -> Unit, onSelect: (id: Int) -> Unit,
onDismiss: () -> Unit, onDismiss: () -> Unit,
@ -39,15 +37,17 @@ fun SettingsRadioDialog(
visible = visible, visible = visible,
title = title, title = title,
text = { text = {
var selectedItemIndex by rememberPrefIntState(key, defaultIndex)
// Modifier.selectableGroup() 用来确保无障碍功能运行正确 // Modifier.selectableGroup() 用来确保无障碍功能运行正确
Column(Modifier.selectableGroup()) { Column(
modifier = Modifier
.selectableGroup()
.verticalScroll(rememberScrollState())
) {
options.forEach { option -> options.forEach { option ->
RadioItem( RadioItem(
selected = option.id == selectedItemIndex, selected = option == currentOptions,
text = option.text, text = option.text,
onClick = { onClick = {
selectedItemIndex = option.id
onSelect(option.id) onSelect(option.id)
onDismiss() onDismiss()
} }
@ -96,11 +96,6 @@ fun RadioItem(
} }
} }
data class SettingsRadioOptions(
val id: Int,
val text: String,
)
@Composable @Composable
fun SettingsDialog( fun SettingsDialog(
modifier: Modifier = Modifier, 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.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.utils.VibrationUtils 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.foundation.layout.padding
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.pages.settings.state.rememberPrefBooleanState
import cn.super12138.todo.utils.VibrationUtils import cn.super12138.todo.utils.VibrationUtils
@Composable @Composable
fun SwitchSettingsItem( fun SwitchSettingsItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
key: String, checked: Boolean,
default: Boolean,
leadingIcon: ImageVector? = null, leadingIcon: ImageVector? = null,
title: String, title: String,
description: String? = null, description: String? = null,
onCheckedChange: (Boolean) -> Unit onCheckedChange: (Boolean) -> Unit
) { ) {
val view = LocalView.current val view = LocalView.current
var switchState by rememberPrefBooleanState(key, default)
SettingsItem( SettingsItem(
leadingIcon = leadingIcon, leadingIcon = leadingIcon,
title = title, title = title,
description = description, description = description,
trailingContent = { trailingContent = {
Switch( Switch(
checked = switchState, checked = checked,
onCheckedChange = { onCheckedChange = {
switchState = it
VibrationUtils.performHapticFeedback(view) VibrationUtils.performHapticFeedback(view)
onCheckedChange(it) onCheckedChange(it)
}, },
@ -40,8 +34,7 @@ fun SwitchSettingsItem(
) )
}, },
onClick = { onClick = {
switchState = !switchState onCheckedChange(!checked)
onCheckedChange(switchState)
}, },
modifier = modifier modifier = modifier
) )

View file

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

View file

@ -27,7 +27,7 @@ import cn.super12138.todo.utils.VibrationUtils
@Composable @Composable
fun DarkModeItem( fun DarkModeItem(
icon: ImageVector, icon: ImageVector,
contentDescription: String, name: String,
contentColor: Color, contentColor: Color,
containerColor: Color, containerColor: Color,
selected: Boolean, selected: Boolean,
@ -73,7 +73,7 @@ fun DarkModeItem(
Spacer(Modifier.size(8.dp)) Spacer(Modifier.size(8.dp))
Text( Text(
text = contentDescription, text = name,
style = MaterialTheme.typography.bodyMedium 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.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cn.super12138.todo.R 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.components.RowSettingsItem
import cn.super12138.todo.ui.pages.settings.state.rememberPrefIntState
import cn.super12138.todo.ui.theme.DarkMode import cn.super12138.todo.ui.theme.DarkMode
@Composable @Composable
fun DarkModePicker( fun DarkModePicker(
currentDarkMode: DarkMode,
onDarkModeChange: (darkMode: DarkMode) -> Unit, onDarkModeChange: (darkMode: DarkMode) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val context = LocalContext.current val context = LocalContext.current
val isInDarkTheme = isSystemInDarkTheme() val isInDarkTheme = isSystemInDarkTheme()
var darkModeState by rememberPrefIntState(
Constants.PREF_DARK_MODE,
Constants.PREF_DARK_MODE_DEFAULT
)
RowSettingsItem( RowSettingsItem(
title = stringResource(R.string.pref_dark_mode), title = stringResource(R.string.pref_dark_mode),
description = stringResource(R.string.pref_dark_mode_desc), description = stringResource(R.string.pref_dark_mode_desc),
@ -38,38 +29,29 @@ fun DarkModePicker(
) { ) {
DarkModeItem( DarkModeItem(
icon = DarkMode.FollowSystem.icon, icon = DarkMode.FollowSystem.icon,
contentDescription = DarkMode.FollowSystem.getDisplayName(context), name = DarkMode.FollowSystem.getDisplayName(context),
contentColor = if (isInDarkTheme) Color.White else Color.Black, contentColor = if (isInDarkTheme) Color.White else Color.Black,
containerColor = if (isInDarkTheme) Color.Black else Color.White, containerColor = if (isInDarkTheme) Color.Black else Color.White,
selected = DarkMode.fromId(darkModeState) == DarkMode.FollowSystem, selected = currentDarkMode == DarkMode.FollowSystem,
onSelect = { onSelect = { onDarkModeChange(DarkMode.FollowSystem) }
darkModeState = DarkMode.FollowSystem.id
onDarkModeChange(DarkMode.FollowSystem)
}
) )
DarkModeItem( DarkModeItem(
icon = DarkMode.Light.icon, icon = DarkMode.Light.icon,
contentDescription = DarkMode.Light.getDisplayName(context), name = DarkMode.Light.getDisplayName(context),
contentColor = Color.Black, contentColor = Color.Black,
containerColor = Color.White, containerColor = Color.White,
selected = DarkMode.fromId(darkModeState) == DarkMode.Light, selected = currentDarkMode == DarkMode.Light,
onSelect = { onSelect = { onDarkModeChange(DarkMode.Light) }
darkModeState = DarkMode.Light.id
onDarkModeChange(DarkMode.Light)
}
) )
DarkModeItem( DarkModeItem(
icon = DarkMode.Dark.icon, icon = DarkMode.Dark.icon,
contentDescription = DarkMode.Dark.getDisplayName(context), name = DarkMode.Dark.getDisplayName(context),
contentColor = Color.White, contentColor = Color.White,
containerColor = Color.Black, containerColor = Color.Black,
selected = DarkMode.fromId(darkModeState) == DarkMode.Dark, selected = currentDarkMode == DarkMode.Dark,
onSelect = { onSelect = { onDarkModeChange(DarkMode.Dark) }
darkModeState = DarkMode.Dark.id
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.semantics.Role
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach 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.ContrastLevel
import cn.super12138.todo.ui.theme.PaletteStyle import cn.super12138.todo.ui.theme.PaletteStyle
import cn.super12138.todo.ui.theme.dynamicColorScheme import cn.super12138.todo.ui.theme.dynamicColorScheme
@ -36,6 +35,7 @@ import cn.super12138.todo.utils.VibrationUtils
@Composable @Composable
fun PaletteItem( fun PaletteItem(
isDynamicColor: Boolean,
isDark: Boolean, isDark: Boolean,
paletteStyle: PaletteStyle, paletteStyle: PaletteStyle,
contrastLevel: ContrastLevel, contrastLevel: ContrastLevel,
@ -63,7 +63,7 @@ fun PaletteItem(
// 为不同主题样式设置不同色板 // 为不同主题样式设置不同色板
MaterialTheme( MaterialTheme(
colorScheme = dynamicColorScheme( 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) colorResource(id = android.R.color.system_accent1_500)
} else { } else {
Color(0xFF0061A4) 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.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cn.super12138.todo.R 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.components.RowSettingsItem
import cn.super12138.todo.ui.pages.settings.state.rememberPrefIntState
import cn.super12138.todo.ui.theme.ContrastLevel import cn.super12138.todo.ui.theme.ContrastLevel
import cn.super12138.todo.ui.theme.DarkMode import cn.super12138.todo.ui.theme.DarkMode
import cn.super12138.todo.ui.theme.PaletteStyle import cn.super12138.todo.ui.theme.PaletteStyle
@Composable @Composable
fun PalettePicker( fun PalettePicker(
currentPalette: PaletteStyle,
onPaletteChange: (paletteStyle: PaletteStyle) -> Unit,
isDynamicColor: Boolean,
isDarkMode: DarkMode, isDarkMode: DarkMode,
contrastLevel: ContrastLevel, contrastLevel: ContrastLevel,
onPaletteChange: (paletteStyle: PaletteStyle) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
var paletteState by rememberPrefIntState(
Constants.PREF_PALETTE_STYLE,
Constants.PREF_PALETTE_STYLE_DEFAULT
)
val paletteOptions = remember { val paletteOptions = remember {
PaletteStyle.entries.toList() PaletteStyle.entries.toList()
@ -42,18 +36,16 @@ fun PalettePicker(
) { ) {
paletteOptions.forEach { paletteStyle -> paletteOptions.forEach { paletteStyle ->
PaletteItem( PaletteItem(
isDynamicColor = isDynamicColor,
isDark = when (isDarkMode) { isDark = when (isDarkMode) {
DarkMode.FollowSystem -> isSystemInDarkTheme() DarkMode.FollowSystem -> isSystemInDarkTheme()
DarkMode.Light -> false DarkMode.Light -> false
DarkMode.Dark -> true DarkMode.Dark -> true
}, },
paletteStyle = paletteStyle, paletteStyle = paletteStyle,
selected = PaletteStyle.fromId(paletteState) == paletteStyle, selected = currentPalette == paletteStyle,
contrastLevel = contrastLevel, contrastLevel = contrastLevel,
onSelect = { onSelect = { onPaletteChange(paletteStyle) }
paletteState = paletteStyle.id
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.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.Typography import androidx.compose.material3.Typography
@ -98,7 +97,7 @@ fun LicenceItem(
modifier = Modifier.padding(padding.licensePadding.contentPadding), modifier = Modifier.padding(padding.licensePadding.contentPadding),
maxLines = 1, maxLines = 1,
text = it.name, text = it.name,
style = MaterialTheme.typography.labelSmall, // style = MaterialTheme.typography.labelSmall,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
overflow = LibraryDefaults.libraryTextStyles().defaultOverflow, 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 androidx.lifecycle.viewModelScope
import cn.super12138.todo.TodoApp import cn.super12138.todo.TodoApp
import cn.super12138.todo.constants.Constants import cn.super12138.todo.constants.Constants
import cn.super12138.todo.constants.GlobalValues
import cn.super12138.todo.logic.Repository import cn.super12138.todo.logic.Repository
import cn.super12138.todo.logic.database.TodoEntity import cn.super12138.todo.logic.database.TodoEntity
import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.logic.model.SortingMethod 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 cn.super12138.todo.utils.FileUtils
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -33,37 +32,27 @@ import java.io.FileOutputStream
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
@OptIn(ExperimentalCoroutinesApi::class)
class MainViewModel : ViewModel() { class MainViewModel : ViewModel() {
// 待办 // 待办
private val toDos: Flow<List<TodoEntity>> = Repository.getAllTodos() private val toDos: Flow<List<TodoEntity>> = Repository.getAllTodos()
var appSortingMethod by mutableStateOf(SortingMethod.fromId(GlobalValues.sortingMethod)) val sortedTodos: Flow<List<TodoEntity>> =
val sortedTodos: Flow<List<TodoEntity>> = toDos.map { list -> DataStoreManager.sortingMethodFlow.flatMapLatest { sortingMethod ->
when (appSortingMethod) { toDos.map { list ->
SortingMethod.Sequential -> list.sortedBy { it.id } when (SortingMethod.fromId(sortingMethod)) {
SortingMethod.Subject -> list.sortedBy { it.subject } SortingMethod.Sequential -> list.sortedBy { it.id }
SortingMethod.Priority -> list.sortedByDescending { it.priority } // 优先级高的在前 SortingMethod.Subject -> list.sortedBy { it.subject }
SortingMethod.Completion -> list.sortedBy { it.isCompleted } // 未完成的在前 SortingMethod.Priority -> list.sortedByDescending { it.priority } // 优先级高的在前
SortingMethod.AlphabeticalAscending -> list.sortedBy { it.content } SortingMethod.Completion -> list.sortedBy { it.isCompleted } // 未完成的在前
SortingMethod.AlphabeticalDescending -> list.sortedByDescending { it.content } SortingMethod.AlphabeticalAscending -> list.sortedBy { it.content }
SortingMethod.AlphabeticalDescending -> list.sortedByDescending { it.content }
}
}
} }
}
val showConfetti = mutableStateOf(false) val showConfetti = mutableStateOf(false)
var selectedEditTodo by mutableStateOf<TodoEntity?>(null) var selectedEditTodo by mutableStateOf<TodoEntity?>(null)
private set private set
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 // 多选逻辑参考https://github.com/X1nto/Mauth
private val _selectedTodoIds = MutableStateFlow(listOf<Int>()) 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) { fun backupAppData(uri: Uri, context: Context, onResult: (completed: Boolean) -> Unit) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val result = runCatching { 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) { fun restoreAppData(uri: Uri, context: Context, onResult: (completed: Boolean) -> Unit) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
val result = runCatching { val result = runCatching {
@ -205,23 +177,36 @@ class MainViewModel : ViewModel() {
} }
} }
/**
* 获取要备份文件的文件列表
* * 数据库文件
* * 数据库的 wal 文件
* * 数据库的 shm 文件
* * DataStore Preferences 文件
*/
private fun getBackupFiles(context: Context): List<File> { private fun getBackupFiles(context: Context): List<File> {
val dbPath = TodoApp.db.openHelper.writableDatabase.path val dbPath = TodoApp.db.openHelper.writableDatabase.path
val prefPath = "${context.filesDir.parent}/shared_prefs" val prefPath = "${context.filesDir}/datastore"
return listOf( return listOf(
context.getDatabasePath(Constants.DB_NAME), // 数据库 context.getDatabasePath(Constants.DB_NAME), // 数据库
File("$dbPath-wal"), // 数据库-wal File("$dbPath-wal"), // 数据库-wal
File("$dbPath-shm"), // 数据库-shm File("$dbPath-shm"), // 数据库-shm
File("$prefPath/${Constants.SP_NAME}.xml") // SharedPreferences File("$prefPath/${Constants.SP_NAME}.preferences_pb") // DataStore Preferences
).filter { it.exists() } ).filter { it.exists() }
} }
/**
* 解压 zip 备份文件
*
* @param zipInputStream 备份文件中每个文件的输入流
* @param context 应用 Context
*/
private fun extractZipEntries(zipInputStream: ZipInputStream, context: Context) { private fun extractZipEntries(zipInputStream: ZipInputStream, context: Context) {
val dbPath = context.getDatabasePath(Constants.DB_NAME).parent 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 -> generateSequence { zipInputStream.nextEntry }.forEach { zipEntry ->
val outputFile = File( val outputFile = File(
if (zipEntry.name.endsWith(".xml")) prefPath else dbPath, if (zipEntry.name.endsWith(".preferences_pb")) prefPath else dbPath,
zipEntry.name zipEntry.name
) )
if (zipEntry.isDirectory) { 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.HapticFeedbackConstants
import android.view.View import android.view.View
import cn.super12138.todo.constants.GlobalValues import cn.super12138.todo.constants.Constants
object VibrationUtils { object VibrationUtils {
private var isEnabled: Boolean = GlobalValues.hapticFeedback private var isEnabled: Boolean = Constants.PREF_HAPTIC_FEEDBACK_DEFAULT
fun setEnabled(enabled: Boolean) { fun setEnabled(enabled: Boolean) {
isEnabled = enabled isEnabled = enabled

View file

@ -3,7 +3,7 @@
coreKtx = "1.16.0" coreKtx = "1.16.0"
splashScreen = "1.2.0-beta02" splashScreen = "1.2.0-beta02"
lifecycleRuntimeKtx = "2.9.1" lifecycleRuntimeKtx = "2.9.1"
# security = "1.1.0-alpha06" datastore = "1.1.7"
# Compose # Compose
activityCompose = "1.10.1" activityCompose = "1.10.1"
composeBom = "2025.06.01" 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-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-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-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 # Compose
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }