Custom color schemes v2 - Part 1

Add data structures and theme editor
This commit is contained in:
MM20
2023-08-21 23:43:45 +02:00
parent 411628c607
commit b0db1707a5
25 changed files with 2949 additions and 53 deletions
+1
View File
@@ -130,6 +130,7 @@ dependencies {
implementation(project(":data:files"))
implementation(project(":data:widgets"))
implementation(project(":data:searchable"))
implementation(project(":data:themes"))
implementation(project(":data:wikipedia"))
implementation(project(":services:badges"))
implementation(project(":core:crashreporter"))
@@ -1,7 +1,6 @@
package de.mm20.launcher2.ui.component
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState
import android.util.Log
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
@@ -20,24 +19,18 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeContent
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.FixedThreshold
import androidx.compose.material.FractionalThreshold
import androidx.compose.material.SwipeableState
import androidx.compose.material.swipeable
import androidx.compose.material3.BottomSheetDefaults
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.LocalAbsoluteTonalElevation
@@ -67,7 +60,6 @@ import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import de.mm20.launcher2.ui.ktx.toDp
import de.mm20.launcher2.ui.ktx.toPixels
import kotlinx.coroutines.launch
import kotlin.math.min
@@ -158,6 +150,7 @@ fun BottomSheetDialog(
LocalAbsoluteTonalElevation provides 0.dp,
) {
Popup(
alignment = Alignment.BottomCenter,
properties = PopupProperties(
dismissOnBackPress = dismissible(),
dismissOnClickOutside = dismissible(),
@@ -168,8 +161,7 @@ fun BottomSheetDialog(
) {
BoxWithConstraints(
modifier = Modifier
.fillMaxSize()
.consumeWindowInsets(WindowInsets.systemBars),
.fillMaxSize(),
propagateMinConstraints = true,
contentAlignment = Alignment.BottomCenter
) {
@@ -221,7 +213,10 @@ fun BottomSheetDialog(
draggableState.updateAnchors(
DraggableAnchors {
SwipeState.Dismiss at 0f
if (hasPeekAnchor) SwipeState.Peek at -min(maxHeightPx * 0.5f, sheetHeight)
if (hasPeekAnchor) SwipeState.Peek at -min(
maxHeightPx * 0.5f,
sheetHeight
)
if (hasFullAnchor) SwipeState.Full at -min(maxHeightPx, sheetHeight)
},
)
@@ -300,7 +295,8 @@ fun BottomSheetDialog(
maxHeightPx.toInt() +
(draggableState.offset
.takeIf { !it.isNaN() }
?.roundToInt() ?: 0).coerceAtLeast(heightPx.toInt())
?.roundToInt()
?: 0).coerceAtLeast(heightPx.toInt())
)
}
.fillMaxWidth(),
@@ -0,0 +1,347 @@
package de.mm20.launcher2.ui.component.colorpicker
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.AbsoluteAlignment
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.ktx.hct
import de.mm20.launcher2.ui.ktx.toHexString
import hct.Hct
import kotlin.math.atan2
import kotlin.math.roundToInt
@Stable
class HctColorPickerState(
initialColor: Color,
val onColorChanged: (Color) -> Unit,
) {
var hue by mutableStateOf(0f)
var chroma by mutableStateOf(0f)
var tone by mutableStateOf(0f)
val color by derivedStateOf {
Color.hct(hue, chroma, tone)
}
internal fun setHue(hue: Float) {
this.hue = hue
onColorChanged(Color.hct(hue, chroma, tone))
}
internal fun setChroma(sat: Float) {
this.chroma = sat
onColorChanged(Color.hct(hue, sat, tone))
}
internal fun setTone(value: Float) {
this.tone = value
onColorChanged(Color.hct(hue, chroma, value))
}
internal fun setColor(color: Color) {
val hct = Hct.fromInt(color.toArgb())
this.hue = hct.hue.toFloat()
this.chroma = hct.chroma.toFloat()
this.tone = hct.tone.toFloat()
onColorChanged(color)
}
init {
val hct = Hct.fromInt(initialColor.toArgb())
this.hue = hct.hue.toFloat()
this.chroma = hct.chroma.toFloat()
this.tone = hct.tone.toFloat()
}
}
@Composable
fun rememberHctColorPickerState(
initialColor: Color,
onColorChanged: (Color) -> Unit
): HctColorPickerState {
return remember {
HctColorPickerState(initialColor, onColorChanged)
}
}
@Composable
fun HctColorPicker(
state: HctColorPickerState,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
BoxWithConstraints(
modifier = Modifier
.widthIn(max = 300.dp)
.align(Alignment.CenterHorizontally)
.padding(horizontal = 32.dp)
.aspectRatio(1f)
) {
val width = this.maxWidth
val height = this.maxHeight
Canvas(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectDragGestures(
onDrag = { change, it ->
val (x, y) = change.position
val angle = atan2(
y.toDouble() - height.toPx() / 2,
x.toDouble() - width.toPx() / 2,
)
val h = (Math.toDegrees(angle) + 360f) % 360f
state.setHue(h.toFloat())
}
)
}
.pointerInput(Unit) {
detectTapGestures {
val (x, y) = it
val angle = atan2(
y.toDouble() - height.toPx() / 2,
x.toDouble() - width.toPx() / 2,
)
val h = (Math.toDegrees(angle) + 360f) % 360f
state.setHue(h.toFloat())
}
}
.padding(8.dp)
) {
drawCircle(
brush = Brush.sweepGradient(
colors = listOf(
Color.hct(0f, state.chroma, state.tone),
Color.hct(60f, state.chroma, state.tone),
Color.hct(120f, state.chroma, state.tone),
Color.hct(180f, state.chroma, state.tone),
Color.hct(240f, state.chroma, state.tone),
Color.hct(300f, state.chroma, state.tone),
Color.hct(360f, state.chroma, state.tone),
)
),
style = Stroke(20.dp.toPx())
)
drawCircle(
color = state.color,
style = Fill,
center = center,
radius = size.minDimension / 2 - 18.dp.toPx()
)
}
Box(
modifier = Modifier
.fillMaxSize()
.rotate(state.hue),
) {
Box(
modifier = Modifier
.size(16.dp)
.shadow(1.dp, CircleShape)
.clip(CircleShape)
.background(Color.White)
.align(AbsoluteAlignment.CenterRight)
)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(top = 16.dp)
) {
Text(
text = "C",
modifier = Modifier.width(32.dp),
style = MaterialTheme.typography.labelMedium,
textAlign = TextAlign.Center
)
Slider(
modifier = Modifier.weight(1f),
value = state.chroma,
valueRange = 0f..150f,
onValueChange = {
state.setChroma(it)
},
track = {
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(20.dp)
) {
drawRoundRect(
brush = Brush.horizontalGradient(
colors = listOf(
Color.hct(state.hue, 0f, state.tone),
Color.hct(state.hue, 10f, state.tone),
Color.hct(state.hue, 20f, state.tone),
Color.hct(state.hue, 30f, state.tone),
Color.hct(state.hue, 40f, state.tone),
Color.hct(state.hue, 50f, state.tone),
Color.hct(state.hue, 60f, state.tone),
Color.hct(state.hue, 70f, state.tone),
Color.hct(state.hue, 80f, state.tone),
Color.hct(state.hue, 90f, state.tone),
Color.hct(state.hue, 100f, state.tone),
Color.hct(state.hue, 110f, state.tone),
Color.hct(state.hue, 120f, state.tone),
Color.hct(state.hue, 130f, state.tone),
Color.hct(state.hue, 140f, state.tone),
Color.hct(state.hue, 150f, state.tone)
)
),
style = Fill,
cornerRadius = CornerRadius(10.dp.toPx(), 10.dp.toPx())
)
}
},
thumb = {
Box(
modifier = Modifier
.padding(vertical = 2.dp, horizontal = 8.dp)
.size(16.dp)
.shadow(1.dp, CircleShape)
.clip(CircleShape)
.background(Color.White)
)
}
)
Text(
text = state.chroma.roundToInt().toString(),
modifier = Modifier.width(32.dp),
style = MaterialTheme.typography.labelMedium,
textAlign = TextAlign.Center,
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "T",
modifier = Modifier.width(32.dp),
style = MaterialTheme.typography.labelMedium,
textAlign = TextAlign.Center
)
Slider(
modifier = Modifier.weight(1f),
value = state.tone,
onValueChange = {
state.setTone(it)
},
valueRange = 0f..100f,
track = {
Canvas(
modifier = Modifier
.fillMaxWidth()
.height(20.dp)
) {
drawRoundRect(
brush = Brush.horizontalGradient(
colors = listOf(
Color.hct(state.hue, state.chroma, 0f),
Color.hct(state.hue, state.chroma, 10f),
Color.hct(state.hue, state.chroma, 20f),
Color.hct(state.hue, state.chroma, 30f),
Color.hct(state.hue, state.chroma, 40f),
Color.hct(state.hue, state.chroma, 50f),
Color.hct(state.hue, state.chroma, 60f),
Color.hct(state.hue, state.chroma, 70f),
Color.hct(state.hue, state.chroma, 80f),
Color.hct(state.hue, state.chroma, 90f),
Color.hct(state.hue, state.chroma, 100f)
)
),
style = Fill,
cornerRadius = CornerRadius(10.dp.toPx(), 10.dp.toPx())
)
}
},
thumb = {
Box(
modifier = Modifier
.padding(vertical = 2.dp, horizontal = 8.dp)
.size(16.dp)
.shadow(1.dp, CircleShape)
.clip(CircleShape)
.background(Color.White)
)
}
)
Text(
text = state.tone.roundToInt().toString(),
modifier = Modifier.width(32.dp),
style = MaterialTheme.typography.labelMedium,
textAlign = TextAlign.Center,
)
}
var hexValue by remember(state.color) {
mutableStateOf(
state.color.toHexString().substring(1)
)
}
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp)
.padding(horizontal = 48.dp),
value = hexValue,
onValueChange = {
if (Regex("[0-9a-fA-F]{0,6}").matches(it)) {
hexValue = it
if (it.length == 6) {
val hex = it.toIntOrNull(16) ?: return@OutlinedTextField
val color = Color(hex).copy(alpha = 1f)
state.setColor(color)
}
}
},
prefix = {
Text(
text = "#",
)
}
)
}
}
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
@@ -27,7 +26,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.AbsoluteAlignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.shadow
@@ -43,7 +41,7 @@ import kotlin.math.atan2
import android.graphics.Color as AndroidColor
@Stable
class ColorPickerState(
class HsvColorPickerState(
initialColor: Color,
val onColorChanged: (Color) -> Unit,
) {
@@ -99,15 +97,15 @@ class ColorPickerState(
}
@Composable
fun rememberColorPickerState(initialColor: Color, onColorChanged: (Color) -> Unit): ColorPickerState {
fun rememberHsvColorPickerState(initialColor: Color, onColorChanged: (Color) -> Unit): HsvColorPickerState {
return remember(initialColor, onColorChanged) {
ColorPickerState(initialColor, onColorChanged)
HsvColorPickerState(initialColor, onColorChanged)
}
}
@Composable
fun ColorPicker(
state: ColorPickerState,
fun HsvColorPicker(
state: HsvColorPickerState,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
@@ -8,9 +8,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.ui.component.colorpicker.ColorPicker
import de.mm20.launcher2.ui.component.colorpicker.rememberColorPickerState
import de.mm20.launcher2.ui.ktx.toHexString
import de.mm20.launcher2.ui.component.colorpicker.HsvColorPicker
import de.mm20.launcher2.ui.component.colorpicker.rememberHsvColorPickerState
@Composable
fun ColorPreference(
@@ -52,10 +51,10 @@ fun ColorPreference(
Column(
modifier = Modifier.fillMaxWidth()
) {
val state = rememberColorPickerState(value ?: Color.Black) {
val state = rememberHsvColorPickerState(value ?: Color.Black) {
color = it
}
ColorPicker(state = state)
HsvColorPicker(state = state)
}
},
confirmButton = {
@@ -1,6 +1,7 @@
package de.mm20.launcher2.ui.ktx
import androidx.compose.ui.graphics.Color
import hct.Hct
import kotlin.math.roundToInt
fun Color.toHexString(): String {
@@ -11,4 +12,9 @@ fun Color.toHexString(): String {
red.toString(16).run { if (length == 1) "0$this" else this } +
green.toString(16).run { if (length == 1) "0$this" else this } +
blue.toString(16).run { if (length == 1) "0$this" else this }
}
fun Color.Companion.hct(hue: Float, chroma: Float, tone: Float): Color {
val hct = Hct.from(hue.toDouble(), chroma.toDouble(), tone.toDouble())
return Color(hct.toInt())
}
@@ -28,6 +28,8 @@ import de.mm20.launcher2.ui.settings.cards.CardsSettingsScreen
import de.mm20.launcher2.ui.settings.clockwidget.ClockWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ColorSchemeSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.CustomColorSchemeSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ThemeSettingsScreen
import de.mm20.launcher2.ui.settings.colorscheme.ThemesSettingsScreen
import de.mm20.launcher2.ui.settings.crashreporter.CrashReportScreen
import de.mm20.launcher2.ui.settings.crashreporter.CrashReporterScreen
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
@@ -53,6 +55,7 @@ import de.mm20.launcher2.ui.theme.LauncherTheme
import de.mm20.launcher2.ui.theme.wallpaperColorsAsState
import org.koin.android.ext.android.inject
import java.net.URLDecoder
import java.util.UUID
class SettingsActivity : BaseActivity() {
@@ -101,6 +104,19 @@ class SettingsActivity : BaseActivity() {
composable("settings/appearance/colorscheme/custom") {
CustomColorSchemeSettingsScreen()
}
composable("settings/appearance/themes") {
ThemesSettingsScreen()
}
composable(
"settings/appearance/themes/{id}",
arguments = listOf(navArgument("id") {
nullable = false
})) {
val id = it.arguments?.getString("id")?.let {
UUID.fromString(it)
} ?: return@composable
ThemeSettingsScreen(id)
}
composable("settings/appearance/cards") {
CardsSettingsScreen()
}
@@ -55,6 +55,18 @@ fun AppearanceSettingsScreen() {
navController?.navigate("settings/appearance/colorscheme")
}
)
Preference(
title = stringResource(id = R.string.preference_screen_colors),
summary = when (colorScheme) {
ColorScheme.Default -> stringResource(R.string.preference_colors_default)
ColorScheme.BlackAndWhite -> stringResource(R.string.preference_colors_bw)
ColorScheme.Custom -> stringResource(R.string.preference_colors_custom)
else -> null
},
onClick = {
navController?.navigate("settings/appearance/themes")
}
)
val font by viewModel.font.collectAsState()
ListPreference(
title = stringResource(R.string.preference_font),
@@ -0,0 +1,134 @@
package de.mm20.launcher2.ui.settings.colorscheme
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.RadioButtonChecked
import androidx.compose.material.icons.rounded.RadioButtonUnchecked
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.locals.LocalNavController
import de.mm20.launcher2.ui.theme.colorscheme.systemCorePalette
@Composable
fun ThemesSettingsScreen() {
val viewModel: ThemesSettingsScreenVM = viewModel()
val navController = LocalNavController.current
val selectedTheme by viewModel.selectedTheme.collectAsStateWithLifecycle(null)
val themes by viewModel.themes.collectAsStateWithLifecycle(emptyList())
val systemPalette = systemCorePalette()
PreferenceScreen(title = stringResource(R.string.preference_screen_colors)) {
item {
PreferenceCategory {
for (theme in themes) {
var showMenu by remember { mutableStateOf(false) }
Preference(
icon = if (theme.id == selectedTheme) Icons.Rounded.RadioButtonChecked else Icons.Rounded.RadioButtonUnchecked,
title = theme.name,
controls = {
IconButton(onClick = { showMenu = true }) {
Icon(Icons.Rounded.MoreVert, null)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false }
) {
DropdownMenuItem(
leadingIcon = {
Icon(Icons.Rounded.Edit, null)
},
text = { Text("Edit") },
onClick = {
navController?.navigate("settings/appearance/themes/${theme.id}")
showMenu = false
}
)
}
}
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp)
.height(8.dp)
.clip(
MaterialTheme.shapes.small.copy(
topStart = CornerSize(0f),
topEnd = CornerSize(0f)
)
)
) {
Box(
modifier = Modifier
.background(Color(theme.corePalette.primary ?: systemPalette.primary))
.weight(1f)
.fillMaxHeight()
)
Box(
modifier = Modifier
.background(Color(theme.corePalette.secondary ?: systemPalette.secondary))
.weight(1f)
.fillMaxHeight()
)
Box(
modifier = Modifier
.background(Color(theme.corePalette.tertiary ?: systemPalette.tertiary))
.weight(1f)
.fillMaxHeight()
)
Box(
modifier = Modifier
.background(Color(theme.corePalette.neutral ?: systemPalette.neutral))
.weight(1f)
.fillMaxHeight()
)
Box(
modifier = Modifier
.background(
Color(theme.corePalette.neutralVariant ?: systemPalette.neutralVariant))
.weight(1f)
.fillMaxHeight()
)
Box(
modifier = Modifier
.background(Color(theme.corePalette.error ?: systemPalette.error))
.weight(1f)
.fillMaxHeight()
)
}
}
}
}
}
}
@@ -0,0 +1,29 @@
package de.mm20.launcher2.ui.settings.colorscheme
import android.util.Log
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.themes.DefaultThemeId
import de.mm20.launcher2.themes.Theme
import de.mm20.launcher2.themes.ThemeRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.UUID
class ThemesSettingsScreenVM: ViewModel(), KoinComponent {
private val themeRepository: ThemeRepository by inject()
val selectedTheme: Flow<UUID?> = flowOf(DefaultThemeId)
val themes: Flow<List<Theme>> = themeRepository.getThemes()
fun getTheme(id: UUID): Flow<Theme?> {
return themeRepository.getTheme(id)
}
fun updateTheme(theme: Theme) {
Log.d("MM20", "updateTheme: $theme")
themeRepository.updateTheme(theme)
}
}
@@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.theme
import android.content.Context
import android.os.Build
import android.util.Log
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.CutCornerShape
@@ -15,9 +16,9 @@ import androidx.compose.ui.unit.dp
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.preferences.Settings.AppearanceSettings
import de.mm20.launcher2.preferences.Settings.AppearanceSettings.Theme
import de.mm20.launcher2.themes.DefaultThemeId
import de.mm20.launcher2.themes.Theme
import de.mm20.launcher2.ui.locals.LocalDarkTheme
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
import de.mm20.launcher2.ui.theme.colorscheme.*
import de.mm20.launcher2.ui.theme.typography.DefaultTypography
import de.mm20.launcher2.ui.theme.typography.getDeviceDefaultTypography
@@ -43,10 +44,10 @@ fun LauncherTheme(
)
val themePreference by remember { dataStore.data.map { it.appearance.theme } }.collectAsState(
Theme.System
AppearanceSettings.Theme.System
)
val darkTheme =
themePreference == Theme.Dark || themePreference == Theme.System && isSystemInDarkTheme()
themePreference == AppearanceSettings.Theme.Dark || themePreference == AppearanceSettings.Theme.System && isSystemInDarkTheme()
val cornerRadius by remember {
dataStore.data.map { it.cards.radius.dp }
@@ -94,7 +95,6 @@ fun colorSchemeAsState(
colorScheme: AppearanceSettings.ColorScheme,
darkTheme: Boolean
): MutableState<ColorScheme> {
val context = LocalContext.current
val dataStore: LauncherDataStore by inject()
when (colorScheme) {
@@ -125,29 +125,14 @@ fun colorSchemeAsState(
return state
}
else -> {
if (Build.VERSION.SDK_INT >= 27 && (Build.VERSION.SDK_INT < 31 || colorScheme == AppearanceSettings.ColorScheme.DebugMaterialYouCompat)) {
val wallpaperColors = LocalWallpaperColors.current
val state = remember(wallpaperColors, darkTheme) {
mutableStateOf(
MaterialYouCompatScheme(wallpaperColors, darkTheme)
)
}
return state
val scheme = if (darkTheme) {
darkColorSchemeOf(Theme(DefaultThemeId, name = ""))
} else {
lightColorSchemeOf(Theme(DefaultThemeId, name = ""))
}
if (Build.VERSION.SDK_INT >= 31) {
return remember(darkTheme) {
mutableStateOf(
if (darkTheme) {
dynamicDarkColorScheme(context)
} else {
dynamicLightColorScheme(context)
}
)
}
return remember(scheme, darkTheme) {
mutableStateOf(scheme)
}
return remember { mutableStateOf(if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme) }
}
}
@@ -1,10 +1,108 @@
package de.mm20.launcher2.ui.theme.colorscheme
import android.os.Build
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import de.mm20.launcher2.preferences.Settings
import de.mm20.launcher2.themes.CorePalette
import de.mm20.launcher2.themes.DefaultDarkColorScheme
import de.mm20.launcher2.themes.DefaultLightColorScheme
import de.mm20.launcher2.themes.FullColorScheme
import de.mm20.launcher2.themes.PartialCorePalette
import de.mm20.launcher2.themes.Theme
import de.mm20.launcher2.themes.get
import de.mm20.launcher2.themes.merge
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
fun CustomColorScheme(colors: Settings.AppearanceSettings.CustomColors.Scheme) : ColorScheme {
@Composable
fun lightColorSchemeOf(theme: Theme): ColorScheme {
return colorSchemeOf(theme.lightColorScheme.merge(DefaultLightColorScheme), theme.corePalette)
}
@Composable
fun darkColorSchemeOf(theme: Theme): ColorScheme {
return colorSchemeOf(theme.darkColorScheme.merge(DefaultDarkColorScheme), theme.corePalette)
}
@Composable
fun colorSchemeOf(colorScheme: FullColorScheme, corePalette: PartialCorePalette): ColorScheme {
val defaultPalette = systemCorePalette()
return remember(colorScheme, corePalette, defaultPalette) {
val mergedCorePalette = corePalette.merge(defaultPalette)
ColorScheme(
primary = Color(colorScheme.primary.get(mergedCorePalette)),
onPrimary = Color(colorScheme.onPrimary.get(mergedCorePalette)),
primaryContainer = Color(colorScheme.primaryContainer.get(mergedCorePalette)),
onPrimaryContainer = Color(colorScheme.onPrimaryContainer.get(mergedCorePalette)),
secondary = Color(colorScheme.secondary.get(mergedCorePalette)),
onSecondary = Color(colorScheme.onSecondary.get(mergedCorePalette)),
secondaryContainer = Color(colorScheme.secondaryContainer.get(mergedCorePalette)),
onSecondaryContainer = Color(colorScheme.onSecondaryContainer.get(mergedCorePalette)),
tertiary = Color(colorScheme.tertiary.get(mergedCorePalette)),
onTertiary = Color(colorScheme.onTertiary.get(mergedCorePalette)),
tertiaryContainer = Color(colorScheme.tertiaryContainer.get(mergedCorePalette)),
onTertiaryContainer = Color(colorScheme.onTertiaryContainer.get(mergedCorePalette)),
error = Color(colorScheme.error.get(mergedCorePalette)),
onError = Color(colorScheme.onError.get(mergedCorePalette)),
errorContainer = Color(colorScheme.errorContainer.get(mergedCorePalette)),
onErrorContainer = Color(colorScheme.onErrorContainer.get(mergedCorePalette)),
surface = Color(colorScheme.surface.get(mergedCorePalette)),
onSurface = Color(colorScheme.onSurface.get(mergedCorePalette)),
onSurfaceVariant = Color(colorScheme.onSurfaceVariant.get(mergedCorePalette)),
outline = Color(colorScheme.outline.get(mergedCorePalette)),
outlineVariant = Color(colorScheme.outlineVariant.get(mergedCorePalette)),
surfaceContainerLowest = Color(colorScheme.surfaceContainerLowest.get(mergedCorePalette)),
surfaceContainerLow = Color(colorScheme.surfaceContainerLow.get(mergedCorePalette)),
surfaceContainer = Color(colorScheme.surfaceContainer.get(mergedCorePalette)),
surfaceContainerHigh = Color(colorScheme.surfaceContainerHigh.get(mergedCorePalette)),
surfaceContainerHighest = Color(colorScheme.surfaceContainerHighest.get(mergedCorePalette)),
surfaceDim = Color(colorScheme.surfaceDim.get(mergedCorePalette)),
surfaceBright = Color(colorScheme.surfaceBright.get(mergedCorePalette)),
inverseOnSurface = Color(colorScheme.inverseOnSurface.get(mergedCorePalette)),
inverseSurface = Color(colorScheme.inverseSurface.get(mergedCorePalette)),
inversePrimary = Color(colorScheme.inversePrimary.get(mergedCorePalette)),
surfaceTint = Color(colorScheme.surfaceTint.get(mergedCorePalette)),
background = Color(colorScheme.background.get(mergedCorePalette)),
onBackground = Color(colorScheme.onBackground.get(mergedCorePalette)),
scrim = Color(colorScheme.scrim.get(mergedCorePalette)),
surfaceVariant = Color(colorScheme.surfaceVariant.get(mergedCorePalette)),
)
}
}
@Composable
fun systemCorePalette(): CorePalette<Int> {
val context = LocalContext.current
if (Build.VERSION.SDK_INT >= 31) {
return CorePalette(
primary = ContextCompat.getColor(context, android.R.color.system_accent1_500),
secondary = ContextCompat.getColor(context, android.R.color.system_accent2_500),
tertiary = ContextCompat.getColor(context, android.R.color.system_accent3_500),
neutral = ContextCompat.getColor(context, android.R.color.system_neutral1_500),
neutralVariant = ContextCompat.getColor(context, android.R.color.system_neutral2_500),
error = 0xFFB3261E.toInt(),
)
}
val wallpaperColors = LocalWallpaperColors.current
return remember(wallpaperColors) {
val corePalette = palettes.CorePalette.of(wallpaperColors.primary.toArgb())
CorePalette(
primary = corePalette.a1.tone(40),
secondary = corePalette.a2.tone(40),
tertiary = corePalette.a3.tone(40),
neutral = corePalette.n1.tone(40),
neutralVariant = corePalette.n2.tone(40),
error = corePalette.error.tone(40),
)
}
}
fun CustomColorScheme(colors: Settings.AppearanceSettings.CustomColors.Scheme): ColorScheme {
return ColorScheme(
primary = Color(colors.primary),
onPrimary = Color(colors.onPrimary),