feat(settings): 支持深色模式
This commit is contained in:
parent
72b75e2353
commit
7ab13cd6f9
11 changed files with 239 additions and 5 deletions
|
|
@ -8,4 +8,7 @@ object Constants {
|
||||||
|
|
||||||
const val PREF_PALETTE_STYLE = "palette_style"
|
const val PREF_PALETTE_STYLE = "palette_style"
|
||||||
const val PREF_PALETTE_STYLE_DEFAULT = 1 // TonalSpot
|
const val PREF_PALETTE_STYLE_DEFAULT = 1 // TonalSpot
|
||||||
|
|
||||||
|
const val PREF_DARK_MODE = "dark_mode"
|
||||||
|
const val PREF_DARK_MODE_DEFAULT = 1 // Follow System
|
||||||
}
|
}
|
||||||
|
|
@ -11,4 +11,8 @@ object GlobalValues {
|
||||||
Constants.PREF_PALETTE_STYLE,
|
Constants.PREF_PALETTE_STYLE,
|
||||||
Constants.PREF_PALETTE_STYLE_DEFAULT
|
Constants.PREF_PALETTE_STYLE_DEFAULT
|
||||||
)
|
)
|
||||||
|
var darkMode: Int by SPDelegates(
|
||||||
|
Constants.PREF_DARK_MODE,
|
||||||
|
Constants.PREF_DARK_MODE_DEFAULT
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
package cn.super12138.todo.logic.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.DarkMode
|
||||||
|
import androidx.compose.material.icons.outlined.LightMode
|
||||||
|
import androidx.compose.material.icons.outlined.SettingsSuggest
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import cn.super12138.todo.R
|
||||||
|
|
||||||
|
enum class DarkMode(
|
||||||
|
val id: Int,
|
||||||
|
val icon: ImageVector
|
||||||
|
) {
|
||||||
|
FollowSystem(-1, Icons.Outlined.SettingsSuggest),
|
||||||
|
Light(1, Icons.Outlined.LightMode),
|
||||||
|
Dark(2, Icons.Outlined.DarkMode);
|
||||||
|
|
||||||
|
fun getDisplayName(context: Context): String {
|
||||||
|
val resId = when (this) {
|
||||||
|
FollowSystem -> R.string.dark_mode_system
|
||||||
|
Light -> R.string.dark_mode_light
|
||||||
|
Dark -> R.string.dark_mode_dark
|
||||||
|
}
|
||||||
|
return context.getString(resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromId(id: Int): DarkMode {
|
||||||
|
return DarkMode.entries.find { it.id == id } ?: FollowSystem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,11 +4,17 @@ import android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import cn.super12138.todo.logic.model.DarkMode.Dark
|
||||||
|
import cn.super12138.todo.logic.model.DarkMode.FollowSystem
|
||||||
|
import cn.super12138.todo.logic.model.DarkMode.Light
|
||||||
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.ToDoTheme
|
import cn.super12138.todo.ui.theme.ToDoTheme
|
||||||
|
|
@ -19,10 +25,22 @@ class MainActivity : ComponentActivity() {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
ToDoTheme {
|
val mainViewModel: MainViewModel = viewModel()
|
||||||
|
val showConfetti = mainViewModel.showConfetti
|
||||||
|
val darkTheme = when (mainViewModel.appDarkMode) {
|
||||||
|
FollowSystem -> isSystemInDarkTheme()
|
||||||
|
Light -> false
|
||||||
|
Dark -> true
|
||||||
|
}
|
||||||
|
// https://github.com/dn0ne/lotus/blob/master/app/src/main/java/com/dn0ne/player/MainActivity.kt#L266
|
||||||
|
LaunchedEffect(mainViewModel.appDarkMode) {
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||||
|
isAppearanceLightStatusBars = !darkTheme
|
||||||
|
isAppearanceLightNavigationBars = !darkTheme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToDoTheme(darkTheme = darkTheme) {
|
||||||
Surface(color = MaterialTheme.colorScheme.background) {
|
Surface(color = MaterialTheme.colorScheme.background) {
|
||||||
val mainViewModel: MainViewModel = viewModel()
|
|
||||||
val showConfetti = mainViewModel.showConfetti
|
|
||||||
TodoNavigation(
|
TodoNavigation(
|
||||||
viewModel = mainViewModel,
|
viewModel = mainViewModel,
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,7 @@ fun TodoNavigation(
|
||||||
|
|
||||||
composable(TodoScreen.SettingsAppearance.name) {
|
composable(TodoScreen.SettingsAppearance.name) {
|
||||||
SettingsAppearance(
|
SettingsAppearance(
|
||||||
|
viewModel = viewModel,
|
||||||
onNavigateUp = { navController.navigateUp() }
|
onNavigateUp = { navController.navigateUp() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,16 @@ import cn.super12138.todo.R
|
||||||
import cn.super12138.todo.constants.Constants
|
import cn.super12138.todo.constants.Constants
|
||||||
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.darkmode.DarkModePicker
|
||||||
import cn.super12138.todo.ui.pages.settings.components.palette.PalettePicker
|
import cn.super12138.todo.ui.pages.settings.components.palette.PalettePicker
|
||||||
import cn.super12138.todo.ui.theme.appPaletteStyle
|
import cn.super12138.todo.ui.theme.appPaletteStyle
|
||||||
import cn.super12138.todo.ui.theme.isDynamicColorEnable
|
import cn.super12138.todo.ui.theme.isDynamicColorEnable
|
||||||
|
import cn.super12138.todo.ui.viewmodels.MainViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsAppearance(
|
fun SettingsAppearance(
|
||||||
|
viewModel: MainViewModel,
|
||||||
onNavigateUp: () -> Unit,
|
onNavigateUp: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
|
@ -40,6 +43,10 @@ fun SettingsAppearance(
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
|
DarkModePicker(onDarkModeChange = { viewModel.setDarkMode(it) })
|
||||||
|
|
||||||
|
PalettePicker(onPaletteChange = { appPaletteStyle = it })
|
||||||
|
|
||||||
SwitchSettingsItem(
|
SwitchSettingsItem(
|
||||||
key = Constants.PREF_DYNAMIC_COLOR,
|
key = Constants.PREF_DYNAMIC_COLOR,
|
||||||
default = Constants.PREF_DYNAMIC_COLOR_DEFAULT,
|
default = Constants.PREF_DYNAMIC_COLOR_DEFAULT,
|
||||||
|
|
@ -48,8 +55,6 @@ fun SettingsAppearance(
|
||||||
description = stringResource(R.string.pref_appearance_dynamic_color_desc),
|
description = stringResource(R.string.pref_appearance_dynamic_color_desc),
|
||||||
onCheckedChange = { isDynamicColorEnable = it },
|
onCheckedChange = { isDynamicColorEnable = it },
|
||||||
)
|
)
|
||||||
|
|
||||||
PalettePicker(onPaletteChange = { appPaletteStyle = it })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
package cn.super12138.todo.ui.pages.settings.components.darkmode
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DarkModeItem(
|
||||||
|
icon: ImageVector,
|
||||||
|
contentDescription: String,
|
||||||
|
contentColor: Color,
|
||||||
|
containerColor: Color,
|
||||||
|
selected: Boolean,
|
||||||
|
onSelect: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val borderWidth by animateDpAsState(if (selected) 3.dp else (-1).dp)
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.clickable { onSelect() }
|
||||||
|
.padding(8.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(90.dp)
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.background(containerColor)
|
||||||
|
.border(
|
||||||
|
width = borderWidth,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
shape = MaterialTheme.shapes.large
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = contentColor,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(30.dp)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.size(8.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = contentDescription,
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package cn.super12138.todo.ui.pages.settings.components.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.logic.model.DarkMode
|
||||||
|
import cn.super12138.todo.ui.pages.settings.components.RowSettingsItem
|
||||||
|
import cn.super12138.todo.ui.pages.settings.state.rememberPrefIntState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DarkModePicker(
|
||||||
|
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),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(5.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
DarkModeItem(
|
||||||
|
icon = DarkMode.FollowSystem.icon,
|
||||||
|
contentDescription = 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)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
DarkModeItem(
|
||||||
|
icon = DarkMode.Light.icon,
|
||||||
|
contentDescription = DarkMode.Light.getDisplayName(context),
|
||||||
|
contentColor = Color.Black,
|
||||||
|
containerColor = Color.White,
|
||||||
|
selected = DarkMode.fromId(darkModeState) == DarkMode.Light,
|
||||||
|
onSelect = {
|
||||||
|
darkModeState = DarkMode.Light.id
|
||||||
|
onDarkModeChange(DarkMode.Light)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
DarkModeItem(
|
||||||
|
icon = DarkMode.Dark.icon,
|
||||||
|
contentDescription = DarkMode.Dark.getDisplayName(context),
|
||||||
|
contentColor = Color.White,
|
||||||
|
containerColor = Color.Black,
|
||||||
|
selected = DarkMode.fromId(darkModeState) == DarkMode.Dark,
|
||||||
|
onSelect = {
|
||||||
|
darkModeState = DarkMode.Dark.id
|
||||||
|
onDarkModeChange(DarkMode.Dark)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,8 +5,10 @@ import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
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.model.DarkMode
|
||||||
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
|
||||||
|
|
@ -21,6 +23,10 @@ class MainViewModel : ViewModel() {
|
||||||
private set
|
private set
|
||||||
val showConfetti = mutableStateOf(false)
|
val showConfetti = mutableStateOf(false)
|
||||||
|
|
||||||
|
// 深色模式
|
||||||
|
var appDarkMode by mutableStateOf(DarkMode.fromId(GlobalValues.darkMode))
|
||||||
|
private set
|
||||||
|
|
||||||
// 多选逻辑参考:https://github.com/X1nto/Mauth
|
// 多选逻辑参考:https://github.com/X1nto/Mauth
|
||||||
private val _selectedTodoIds = MutableStateFlow(listOf<Int>())
|
private val _selectedTodoIds = MutableStateFlow(listOf<Int>())
|
||||||
val selectedTodoIds = _selectedTodoIds.asStateFlow()
|
val selectedTodoIds = _selectedTodoIds.asStateFlow()
|
||||||
|
|
@ -108,4 +114,8 @@ class MainViewModel : ViewModel() {
|
||||||
fun playConfetti() {
|
fun playConfetti() {
|
||||||
showConfetti.value = true
|
showConfetti.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setDarkMode(darkMode: DarkMode) {
|
||||||
|
appDarkMode = darkMode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,4 +61,9 @@
|
||||||
<string name="palette_monochrome">单色</string>
|
<string name="palette_monochrome">单色</string>
|
||||||
<string name="palette_fidelity">高保真</string>
|
<string name="palette_fidelity">高保真</string>
|
||||||
<string name="palette_content">内容</string>
|
<string name="palette_content">内容</string>
|
||||||
|
<string name="pref_dark_mode">深色模式</string>
|
||||||
|
<string name="pref_dark_mode_desc">开启或关闭应用深色模式</string>
|
||||||
|
<string name="dark_mode_system">跟随系统</string>
|
||||||
|
<string name="dark_mode_light">浅色模式</string>
|
||||||
|
<string name="dark_mode_dark">深色模式</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -62,4 +62,9 @@
|
||||||
<string name="palette_monochrome">Monochrome</string>
|
<string name="palette_monochrome">Monochrome</string>
|
||||||
<string name="palette_fidelity">Fidelity</string>
|
<string name="palette_fidelity">Fidelity</string>
|
||||||
<string name="palette_content">Content</string>
|
<string name="palette_content">Content</string>
|
||||||
|
<string name="pref_dark_mode">Dark Mode</string>
|
||||||
|
<string name="pref_dark_mode_desc">Enable or disable the dark mode for this app</string>
|
||||||
|
<string name="dark_mode_system">Follow System</string>
|
||||||
|
<string name="dark_mode_light">Light</string>
|
||||||
|
<string name="dark_mode_dark">Dark</string>
|
||||||
</resources>
|
</resources>
|
||||||
Loading…
Reference in a new issue