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_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_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.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.view.WindowCompat
|
||||
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.navigation.TodoNavigation
|
||||
import cn.super12138.todo.ui.theme.ToDoTheme
|
||||
|
|
@ -19,10 +25,22 @@ class MainActivity : ComponentActivity() {
|
|||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
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) {
|
||||
val mainViewModel: MainViewModel = viewModel()
|
||||
val showConfetti = mainViewModel.showConfetti
|
||||
TodoNavigation(
|
||||
viewModel = mainViewModel,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ fun TodoNavigation(
|
|||
|
||||
composable(TodoScreen.SettingsAppearance.name) {
|
||||
SettingsAppearance(
|
||||
viewModel = viewModel,
|
||||
onNavigateUp = { navController.navigateUp() }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,13 +17,16 @@ import cn.super12138.todo.R
|
|||
import cn.super12138.todo.constants.Constants
|
||||
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
|
||||
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.theme.appPaletteStyle
|
||||
import cn.super12138.todo.ui.theme.isDynamicColorEnable
|
||||
import cn.super12138.todo.ui.viewmodels.MainViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsAppearance(
|
||||
viewModel: MainViewModel,
|
||||
onNavigateUp: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
|
|
@ -40,6 +43,10 @@ fun SettingsAppearance(
|
|||
.padding(innerPadding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
DarkModePicker(onDarkModeChange = { viewModel.setDarkMode(it) })
|
||||
|
||||
PalettePicker(onPaletteChange = { appPaletteStyle = it })
|
||||
|
||||
SwitchSettingsItem(
|
||||
key = Constants.PREF_DYNAMIC_COLOR,
|
||||
default = Constants.PREF_DYNAMIC_COLOR_DEFAULT,
|
||||
|
|
@ -48,8 +55,6 @@ fun SettingsAppearance(
|
|||
description = stringResource(R.string.pref_appearance_dynamic_color_desc),
|
||||
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.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import cn.super12138.todo.constants.GlobalValues
|
||||
import cn.super12138.todo.logic.Repository
|
||||
import cn.super12138.todo.logic.database.TodoEntity
|
||||
import cn.super12138.todo.logic.model.DarkMode
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
|
|
@ -21,6 +23,10 @@ class MainViewModel : ViewModel() {
|
|||
private set
|
||||
val showConfetti = mutableStateOf(false)
|
||||
|
||||
// 深色模式
|
||||
var appDarkMode by mutableStateOf(DarkMode.fromId(GlobalValues.darkMode))
|
||||
private set
|
||||
|
||||
// 多选逻辑参考:https://github.com/X1nto/Mauth
|
||||
private val _selectedTodoIds = MutableStateFlow(listOf<Int>())
|
||||
val selectedTodoIds = _selectedTodoIds.asStateFlow()
|
||||
|
|
@ -108,4 +114,8 @@ class MainViewModel : ViewModel() {
|
|||
fun playConfetti() {
|
||||
showConfetti.value = true
|
||||
}
|
||||
|
||||
fun setDarkMode(darkMode: DarkMode) {
|
||||
appDarkMode = darkMode
|
||||
}
|
||||
}
|
||||
|
|
@ -61,4 +61,9 @@
|
|||
<string name="palette_monochrome">单色</string>
|
||||
<string name="palette_fidelity">高保真</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>
|
||||
|
|
@ -62,4 +62,9 @@
|
|||
<string name="palette_monochrome">Monochrome</string>
|
||||
<string name="palette_fidelity">Fidelity</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>
|
||||
Loading…
Reference in a new issue