refactor: 用 Preference DataStore 代替 Preference
This commit is contained in:
parent
65258660cd
commit
d7eec36fc8
25 changed files with 344 additions and 390 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ fun Candle(
|
|||
) {
|
||||
// 火苗
|
||||
Box {
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
AnimatedVisibility(
|
||||
visible = showFlame,
|
||||
enter = scaleIn(),
|
||||
exit = scaleOut(),
|
||||
|
|
|
|||
|
|
@ -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) } }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
|
|
@ -5,14 +5,14 @@ import androidx.compose.foundation.layout.Row
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
|
|
@ -21,15 +21,13 @@ import androidx.compose.ui.semantics.Role
|
|||
import androidx.compose.ui.unit.dp
|
||||
import cn.super12138.todo.ui.TodoDefaults
|
||||
import cn.super12138.todo.ui.components.BasicDialog
|
||||
import cn.super12138.todo.ui.pages.settings.state.rememberPrefIntState
|
||||
import cn.super12138.todo.utils.VibrationUtils
|
||||
|
||||
@Composable
|
||||
fun SettingsRadioDialog(
|
||||
key: String,
|
||||
defaultIndex: Int,
|
||||
visible: Boolean,
|
||||
title: String,
|
||||
currentOptions: SettingsRadioOptions,
|
||||
options: List<SettingsRadioOptions>,
|
||||
onSelect: (id: Int) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
|
|
@ -39,15 +37,17 @@ fun SettingsRadioDialog(
|
|||
visible = visible,
|
||||
title = title,
|
||||
text = {
|
||||
var selectedItemIndex by rememberPrefIntState(key, defaultIndex)
|
||||
// Modifier.selectableGroup() 用来确保无障碍功能运行正确
|
||||
Column(Modifier.selectableGroup()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.selectableGroup()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
options.forEach { option ->
|
||||
RadioItem(
|
||||
selected = option.id == selectedItemIndex,
|
||||
selected = option == currentOptions,
|
||||
text = option.text,
|
||||
onClick = {
|
||||
selectedItemIndex = option.id
|
||||
onSelect(option.id)
|
||||
onDismiss()
|
||||
}
|
||||
|
|
@ -96,11 +96,6 @@ fun RadioItem(
|
|||
}
|
||||
}
|
||||
|
||||
data class SettingsRadioOptions(
|
||||
val id: Int,
|
||||
val text: String,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SettingsDialog(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package cn.super12138.todo.ui.pages.settings.components
|
||||
|
||||
data class SettingsRadioOptions(
|
||||
val id: Int,
|
||||
val text: String,
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -9,19 +9,18 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import cn.super12138.todo.TodoApp
|
||||
import cn.super12138.todo.constants.Constants
|
||||
import cn.super12138.todo.constants.GlobalValues
|
||||
import cn.super12138.todo.logic.Repository
|
||||
import cn.super12138.todo.logic.database.TodoEntity
|
||||
import cn.super12138.todo.logic.datastore.DataStoreManager
|
||||
import cn.super12138.todo.logic.model.SortingMethod
|
||||
import cn.super12138.todo.ui.theme.ContrastLevel
|
||||
import cn.super12138.todo.ui.theme.DarkMode
|
||||
import cn.super12138.todo.ui.theme.PaletteStyle
|
||||
import cn.super12138.todo.utils.FileUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -33,37 +32,27 @@ import java.io.FileOutputStream
|
|||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class MainViewModel : ViewModel() {
|
||||
// 待办
|
||||
private val toDos: Flow<List<TodoEntity>> = Repository.getAllTodos()
|
||||
var appSortingMethod by mutableStateOf(SortingMethod.fromId(GlobalValues.sortingMethod))
|
||||
val sortedTodos: Flow<List<TodoEntity>> = toDos.map { list ->
|
||||
when (appSortingMethod) {
|
||||
SortingMethod.Sequential -> list.sortedBy { it.id }
|
||||
SortingMethod.Subject -> list.sortedBy { it.subject }
|
||||
SortingMethod.Priority -> list.sortedByDescending { it.priority } // 优先级高的在前
|
||||
SortingMethod.Completion -> list.sortedBy { it.isCompleted } // 未完成的在前
|
||||
SortingMethod.AlphabeticalAscending -> list.sortedBy { it.content }
|
||||
SortingMethod.AlphabeticalDescending -> list.sortedByDescending { it.content }
|
||||
val sortedTodos: Flow<List<TodoEntity>> =
|
||||
DataStoreManager.sortingMethodFlow.flatMapLatest { sortingMethod ->
|
||||
toDos.map { list ->
|
||||
when (SortingMethod.fromId(sortingMethod)) {
|
||||
SortingMethod.Sequential -> list.sortedBy { it.id }
|
||||
SortingMethod.Subject -> list.sortedBy { it.subject }
|
||||
SortingMethod.Priority -> list.sortedByDescending { it.priority } // 优先级高的在前
|
||||
SortingMethod.Completion -> list.sortedBy { it.isCompleted } // 未完成的在前
|
||||
SortingMethod.AlphabeticalAscending -> list.sortedBy { it.content }
|
||||
SortingMethod.AlphabeticalDescending -> list.sortedByDescending { it.content }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val showConfetti = mutableStateOf(false)
|
||||
var selectedEditTodo by mutableStateOf<TodoEntity?>(null)
|
||||
private set
|
||||
var showCompletedTodos by mutableStateOf(GlobalValues.showCompleted)
|
||||
|
||||
// 主题颜色
|
||||
var appDynamicColorEnable by mutableStateOf(GlobalValues.dynamicColor)
|
||||
private set
|
||||
var appPaletteStyle by mutableStateOf(PaletteStyle.fromId(GlobalValues.paletteStyle))
|
||||
private set
|
||||
var appDarkMode by mutableStateOf(DarkMode.fromId(GlobalValues.darkMode))
|
||||
private set
|
||||
var appContrastLevel by mutableStateOf(ContrastLevel.fromFloat(GlobalValues.contrastLevel))
|
||||
private set
|
||||
var appSecureMode by mutableStateOf(GlobalValues.secureMode)
|
||||
private set
|
||||
|
||||
// 多选逻辑参考:https://github.com/X1nto/Mauth
|
||||
private val _selectedTodoIds = MutableStateFlow(listOf<Int>())
|
||||
|
|
@ -147,36 +136,12 @@ class MainViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
/**
|
||||
* 应用设置
|
||||
* 备份应用数据
|
||||
*
|
||||
* @param uri 备份文件路径的 URI
|
||||
* @param context 应用 Context
|
||||
* @param onResult 备份完成的回调函数
|
||||
*/
|
||||
fun setDynamicColor(enabled: Boolean) {
|
||||
appDynamicColorEnable = enabled
|
||||
}
|
||||
|
||||
fun setPaletteStyle(paletteStyle: PaletteStyle) {
|
||||
appPaletteStyle = paletteStyle
|
||||
}
|
||||
|
||||
fun setDarkMode(darkMode: DarkMode) {
|
||||
appDarkMode = darkMode
|
||||
}
|
||||
|
||||
fun setContrastLevel(contrastLevel: ContrastLevel) {
|
||||
appContrastLevel = contrastLevel
|
||||
}
|
||||
|
||||
fun setShowCompleted(show: Boolean) {
|
||||
showCompletedTodos = show
|
||||
}
|
||||
|
||||
fun setSortingMethod(sortingMethod: SortingMethod) {
|
||||
appSortingMethod = sortingMethod
|
||||
}
|
||||
|
||||
fun setSecureMode(enabled: Boolean) {
|
||||
appSecureMode = enabled
|
||||
}
|
||||
|
||||
fun backupAppData(uri: Uri, context: Context, onResult: (completed: Boolean) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val result = runCatching {
|
||||
|
|
@ -192,6 +157,13 @@ class MainViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复应用数据
|
||||
*
|
||||
* @param uri 选择的恢复文件的 URI
|
||||
* @param context 应用 Context
|
||||
* @param onResult 恢复完成的回调函数
|
||||
*/
|
||||
fun restoreAppData(uri: Uri, context: Context, onResult: (completed: Boolean) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val result = runCatching {
|
||||
|
|
@ -205,23 +177,36 @@ class MainViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取要备份文件的文件列表
|
||||
* * 数据库文件
|
||||
* * 数据库的 wal 文件
|
||||
* * 数据库的 shm 文件
|
||||
* * DataStore Preferences 文件
|
||||
*/
|
||||
private fun getBackupFiles(context: Context): List<File> {
|
||||
val dbPath = TodoApp.db.openHelper.writableDatabase.path
|
||||
val prefPath = "${context.filesDir.parent}/shared_prefs"
|
||||
val prefPath = "${context.filesDir}/datastore"
|
||||
return listOf(
|
||||
context.getDatabasePath(Constants.DB_NAME), // 数据库
|
||||
File("$dbPath-wal"), // 数据库-wal
|
||||
File("$dbPath-shm"), // 数据库-shm
|
||||
File("$prefPath/${Constants.SP_NAME}.xml") // SharedPreferences
|
||||
File("$prefPath/${Constants.SP_NAME}.preferences_pb") // DataStore Preferences
|
||||
).filter { it.exists() }
|
||||
}
|
||||
|
||||
/**
|
||||
* 解压 zip 备份文件
|
||||
*
|
||||
* @param zipInputStream 备份文件中每个文件的输入流
|
||||
* @param context 应用 Context
|
||||
*/
|
||||
private fun extractZipEntries(zipInputStream: ZipInputStream, context: Context) {
|
||||
val dbPath = context.getDatabasePath(Constants.DB_NAME).parent
|
||||
val prefPath = "${context.filesDir.parent}/shared_prefs/"
|
||||
val prefPath = "${context.filesDir}/datastore/"
|
||||
generateSequence { zipInputStream.nextEntry }.forEach { zipEntry ->
|
||||
val outputFile = File(
|
||||
if (zipEntry.name.endsWith(".xml")) prefPath else dbPath,
|
||||
if (zipEntry.name.endsWith(".preferences_pb")) prefPath else dbPath,
|
||||
zipEntry.name
|
||||
)
|
||||
if (zipEntry.isDirectory) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
Loading…
Reference in a new issue