Custom color schemes v2 - Part 1

Add data structures and theme editor
This commit is contained in:
MM20 2023-08-21 22:11:31 +02:00
parent 411628c607
commit b0db1707a5
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
25 changed files with 2949 additions and 53 deletions

View File

@ -134,6 +134,7 @@ dependencies {
implementation(project(":data:currencies"))
implementation(project(":data:customattrs"))
implementation(project(":data:searchable"))
implementation(project(":data:themes"))
implementation(project(":data:files"))
implementation(project(":libs:g-services"))
implementation(project(":core:i18n"))

View File

@ -32,6 +32,7 @@ import de.mm20.launcher2.searchactions.searchActionsModule
import de.mm20.launcher2.services.favorites.favoritesModule
import de.mm20.launcher2.services.tags.servicesTagsModule
import de.mm20.launcher2.services.widgets.widgetsServiceModule
import de.mm20.launcher2.themes.themesModule
import de.mm20.launcher2.weather.weatherModule
import kotlinx.coroutines.*
import org.koin.android.ext.koin.androidContext
@ -78,6 +79,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
preferencesModule,
searchModule,
searchActionsModule,
themesModule,
unitConverterModule,
weatherModule,
websitesModule,

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"))

View File

@ -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(),

View File

@ -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 = "#",
)
}
)
}
}

View File

@ -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) {

View File

@ -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 = {

View File

@ -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 {
@ -12,3 +13,8 @@ fun Color.toHexString(): String {
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())
}

View File

@ -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()
}

View File

@ -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),

View File

@ -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()
)
}
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -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,30 +125,15 @@ 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
}
if (Build.VERSION.SDK_INT >= 31) {
return remember(darkTheme) {
mutableStateOf(
if (darkTheme) {
dynamicDarkColorScheme(context)
val scheme = if (darkTheme) {
darkColorSchemeOf(Theme(DefaultThemeId, name = ""))
} else {
dynamicLightColorScheme(context)
lightColorSchemeOf(Theme(DefaultThemeId, name = ""))
}
)
return remember(scheme, darkTheme) {
mutableStateOf(scheme)
}
}
return remember { mutableStateOf(if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme) }
}
}
}

View File

@ -1,8 +1,106 @@
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
@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(

View File

@ -436,6 +436,7 @@
<string name="preference_custom_colors_n1">Neutral</string>
<string name="preference_custom_colors_n2">Neutral Variant</string>
<string name="preference_custom_colors_error">Error</string>
<string name="preference_custom_colors_corepalette">Color palette</string>
<string name="preference_custom_colors_advanced_mode">Advanced mode</string>
<string name="preference_custom_colors_simple_mode">Simple mode</string>
<string name="preference_colors_auto_generate">Generate from primary color</string>

1
data/themes/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,50 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "de.mm20.launcher2.themes"
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.bundles.androidx.lifecycle)
implementation(libs.koin.android)
implementation(project(":core:base"))
implementation(project(":core:database"))
implementation(project(":core:crashreporter"))
implementation(project(":libs:material-color-utilities"))
}

View File

21
data/themes/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1 @@
<manifest />

View File

@ -0,0 +1,84 @@
package de.mm20.launcher2.themes
import java.util.UUID
val DefaultThemeId = UUID(0L, 0L)
val DefaultLightColorScheme = ColorScheme<Color>(
primary = ColorRef(CorePaletteColor.Primary, 40),
onPrimary = ColorRef(CorePaletteColor.Primary, 100),
primaryContainer = ColorRef(CorePaletteColor.Primary, 90),
onPrimaryContainer = ColorRef(CorePaletteColor.Primary, 10),
secondary = ColorRef(CorePaletteColor.Secondary, 40),
onSecondary = ColorRef(CorePaletteColor.Secondary, 100),
secondaryContainer = ColorRef(CorePaletteColor.Secondary, 90),
onSecondaryContainer = ColorRef(CorePaletteColor.Secondary, 10),
tertiary = ColorRef(CorePaletteColor.Tertiary, 40),
onTertiary = ColorRef(CorePaletteColor.Tertiary, 100),
tertiaryContainer = ColorRef(CorePaletteColor.Tertiary, 90),
onTertiaryContainer = ColorRef(CorePaletteColor.Tertiary, 10),
error = ColorRef(CorePaletteColor.Error, 40),
onError = ColorRef(CorePaletteColor.Error, 100),
errorContainer = ColorRef(CorePaletteColor.Error, 90),
onErrorContainer = ColorRef(CorePaletteColor.Error, 10),
surfaceDim = ColorRef(CorePaletteColor.Neutral, 87),
surface = ColorRef(CorePaletteColor.Neutral, 98),
surfaceBright = ColorRef(CorePaletteColor.Neutral, 98),
surfaceContainerLowest = ColorRef(CorePaletteColor.Neutral, 100),
surfaceContainerLow = ColorRef(CorePaletteColor.Neutral, 96),
surfaceContainer = ColorRef(CorePaletteColor.Neutral, 94),
surfaceContainerHigh = ColorRef(CorePaletteColor.Neutral, 92),
surfaceContainerHighest = ColorRef(CorePaletteColor.Neutral, 90),
onSurface = ColorRef(CorePaletteColor.Neutral, 10),
onSurfaceVariant = ColorRef(CorePaletteColor.NeutralVariant, 30),
outline = ColorRef(CorePaletteColor.NeutralVariant, 50),
outlineVariant = ColorRef(CorePaletteColor.NeutralVariant, 80),
inverseSurface = ColorRef(CorePaletteColor.Neutral, 20),
inverseOnSurface = ColorRef(CorePaletteColor.Neutral, 95),
inversePrimary = ColorRef(CorePaletteColor.Primary, 80),
surfaceVariant = ColorRef(CorePaletteColor.NeutralVariant, 90),
surfaceTint = ColorRef(CorePaletteColor.Primary, 40),
background = ColorRef(CorePaletteColor.Neutral, 98),
onBackground = ColorRef(CorePaletteColor.Neutral, 10),
scrim = ColorRef(CorePaletteColor.Neutral, 0),
)
val DefaultDarkColorScheme = ColorScheme<Color>(
primary = ColorRef(CorePaletteColor.Primary, 80),
onPrimary = ColorRef(CorePaletteColor.Primary, 20),
primaryContainer = ColorRef(CorePaletteColor.Primary, 30),
onPrimaryContainer = ColorRef(CorePaletteColor.Primary, 90),
secondary = ColorRef(CorePaletteColor.Secondary, 80),
onSecondary = ColorRef(CorePaletteColor.Secondary, 20),
secondaryContainer = ColorRef(CorePaletteColor.Secondary, 30),
onSecondaryContainer = ColorRef(CorePaletteColor.Secondary, 90),
tertiary = ColorRef(CorePaletteColor.Tertiary, 80),
onTertiary = ColorRef(CorePaletteColor.Tertiary, 20),
tertiaryContainer = ColorRef(CorePaletteColor.Tertiary, 30),
onTertiaryContainer = ColorRef(CorePaletteColor.Tertiary, 90),
error = ColorRef(CorePaletteColor.Error, 80),
onError = ColorRef(CorePaletteColor.Error, 20),
errorContainer = ColorRef(CorePaletteColor.Error, 30),
onErrorContainer = ColorRef(CorePaletteColor.Error, 90),
surfaceDim = ColorRef(CorePaletteColor.Neutral, 6),
surface = ColorRef(CorePaletteColor.Neutral, 6),
surfaceBright = ColorRef(CorePaletteColor.Neutral, 24),
surfaceContainerLowest = ColorRef(CorePaletteColor.Neutral, 4),
surfaceContainerLow = ColorRef(CorePaletteColor.Neutral, 10),
surfaceContainer = ColorRef(CorePaletteColor.Neutral, 12),
surfaceContainerHigh = ColorRef(CorePaletteColor.Neutral, 17),
surfaceContainerHighest = ColorRef(CorePaletteColor.Neutral, 22),
onSurface = ColorRef(CorePaletteColor.Neutral, 90),
onSurfaceVariant = ColorRef(CorePaletteColor.NeutralVariant, 80),
outline = ColorRef(CorePaletteColor.NeutralVariant, 60),
outlineVariant = ColorRef(CorePaletteColor.NeutralVariant, 30),
inverseSurface = ColorRef(CorePaletteColor.Neutral, 98),
inverseOnSurface = ColorRef(CorePaletteColor.Neutral, 10),
inversePrimary = ColorRef(CorePaletteColor.Primary, 40),
surfaceVariant = ColorRef(CorePaletteColor.NeutralVariant, 30),
surfaceTint = ColorRef(CorePaletteColor.Primary, 80),
background = ColorRef(CorePaletteColor.Neutral, 6),
onBackground = ColorRef(CorePaletteColor.Neutral, 90),
scrim = ColorRef(CorePaletteColor.Neutral, 0),
)

View File

@ -0,0 +1,7 @@
package de.mm20.launcher2.themes
import org.koin.dsl.module
val themesModule = module {
factory { ThemeRepository(get()) }
}

View File

@ -0,0 +1,186 @@
package de.mm20.launcher2.themes
import hct.Hct
import java.util.UUID
enum class CorePaletteColor {
Primary,
Secondary,
Tertiary,
Neutral,
NeutralVariant,
Error;
override fun toString(): String {
return when (this) {
Primary -> "p"
Secondary -> "s"
Tertiary -> "t"
Neutral -> "n"
NeutralVariant -> "nv"
Error -> "e"
}
}
}
sealed interface Color
data class ColorRef(
val color: CorePaletteColor,
val tone: Int,
) : Color {
override fun toString(): String {
return "\$${color.name}.$tone"
}
}
@JvmInline
value class StaticColor(val color: Int) : Color {
override fun toString(): String {
return "#${color.toString(16).padStart(6, '0')}"
}
}
data class CorePalette<out T : Int?>(
val primary: T,
val secondary: T,
val tertiary: T,
val neutral: T,
val neutralVariant: T,
val error: T,
)
val EmptyCorePalette = CorePalette<Int?>(null, null, null, null, null, null)
typealias FullCorePalette = CorePalette<Int>
typealias PartialCorePalette = CorePalette<Int?>
data class ColorScheme<out T: Color?>(
val primary: T,
val onPrimary: T,
val primaryContainer: T,
val onPrimaryContainer: T,
val secondary: T,
val onSecondary: T,
val secondaryContainer: T,
val onSecondaryContainer: T,
val tertiary: T,
val onTertiary: T,
val tertiaryContainer: T,
val onTertiaryContainer: T,
val error: T,
val onError: T,
val errorContainer: T,
val onErrorContainer: T,
val surface: T,
val onSurface: T,
val onSurfaceVariant: T,
val outline: T,
val outlineVariant: T,
val inverseSurface: T,
val inverseOnSurface: T,
val inversePrimary: T,
val surfaceDim: T,
val surfaceBright: T,
val surfaceContainerLowest: T,
val surfaceContainerLow: T,
val surfaceContainer: T,
val surfaceContainerHigh: T,
val surfaceContainerHighest: T,
val background: T,
val onBackground: T,
val surfaceTint: T,
val scrim: T,
val surfaceVariant: T,
)
typealias FullColorScheme = ColorScheme<Color>
typealias PartialColorScheme = ColorScheme<Color?>
data class Theme(
val id: UUID,
val builtIn: Boolean = false,
val name: String,
val corePalette: PartialCorePalette = EmptyCorePalette,
val lightColorScheme: PartialColorScheme = DefaultLightColorScheme,
val darkColorScheme: PartialColorScheme = DefaultDarkColorScheme,
)
fun <T : Int?> CorePalette<T>.get(color: CorePaletteColor): T {
return when (color) {
CorePaletteColor.Primary -> primary
CorePaletteColor.Secondary -> secondary
CorePaletteColor.Tertiary -> tertiary
CorePaletteColor.Neutral -> neutral
CorePaletteColor.NeutralVariant -> neutralVariant
CorePaletteColor.Error -> error
}
}
fun Color.get(corePalette: FullCorePalette): Int {
return when (this) {
is StaticColor -> color
is ColorRef -> {
corePalette.get(this.color).atTone(this.tone)
}
}
}
fun Int.atTone(tone: Int): Int {
return Hct.fromInt(this).apply {
setTone(tone.toDouble())
}.toInt()
}
fun PartialCorePalette.merge(other: FullCorePalette): FullCorePalette {
return CorePalette(
primary = this.primary ?: other.primary,
secondary = this.secondary ?: other.secondary,
tertiary = this.tertiary ?: other.tertiary,
neutral = this.neutral ?: other.neutral,
neutralVariant = this.neutralVariant ?: other.neutralVariant,
error = this.error ?: other.error,
)
}
fun PartialColorScheme.merge(other: FullColorScheme): FullColorScheme {
return ColorScheme(
primary = this.primary ?: other.primary,
onPrimary = this.onPrimary ?: other.onPrimary,
primaryContainer = this.primaryContainer ?: other.primaryContainer,
onPrimaryContainer = this.onPrimaryContainer ?: other.onPrimaryContainer,
secondary = this.secondary ?: other.secondary,
onSecondary = this.onSecondary ?: other.onSecondary,
secondaryContainer = this.secondaryContainer ?: other.secondaryContainer,
onSecondaryContainer = this.onSecondaryContainer ?: other.onSecondaryContainer,
tertiary = this.tertiary ?: other.tertiary,
onTertiary = this.onTertiary ?: other.onTertiary,
tertiaryContainer = this.tertiaryContainer ?: other.tertiaryContainer,
onTertiaryContainer = this.onTertiaryContainer ?: other.onTertiaryContainer,
error = this.error ?: other.error,
onError = this.onError ?: other.onError,
errorContainer = this.errorContainer ?: other.errorContainer,
onErrorContainer = this.onErrorContainer ?: other.onErrorContainer,
surfaceDim = this.surfaceDim ?: other.surfaceDim,
surface = this.surface ?: other.surface,
surfaceBright = this.surfaceBright ?: other.surfaceBright,
surfaceContainerLowest = this.surfaceContainerLowest ?: other.surfaceContainerLowest,
surfaceContainerLow = this.surfaceContainerLow ?: other.surfaceContainerLow,
surfaceContainer = this.surfaceContainer ?: other.surfaceContainer,
surfaceContainerHigh = this.surfaceContainerHigh ?: other.surfaceContainerHigh,
surfaceContainerHighest = this.surfaceContainerHighest ?: other.surfaceContainerHighest,
onSurface = this.onSurface ?: other.onSurface,
onSurfaceVariant = this.onSurfaceVariant ?: other.onSurfaceVariant,
outline = this.outline ?: other.outline,
outlineVariant = this.outlineVariant ?: other.outlineVariant,
inverseSurface = this.inverseSurface ?: other.inverseSurface,
inverseOnSurface = this.inverseOnSurface ?: other.inverseOnSurface,
inversePrimary = this.inversePrimary ?: other.inversePrimary,
surfaceVariant = this.surfaceVariant ?: other.surfaceVariant,
scrim = this.scrim ?: other.scrim,
onBackground = this.onBackground ?: other.onBackground,
background = this.background ?: other.background,
surfaceTint = this.surfaceTint ?: other.surfaceTint,
)
}

View File

@ -0,0 +1,65 @@
package de.mm20.launcher2.themes
import android.content.Context
import android.util.Log
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import java.util.UUID
class ThemeRepository(
private val context: Context,
) {
private val customTheme = MutableStateFlow(Theme(
id = UUID.randomUUID(),
builtIn = false,
name = "Custom",
corePalette = EmptyCorePalette,
lightColorScheme = DefaultLightColorScheme,
darkColorScheme = DefaultDarkColorScheme,
))
fun getThemes(): Flow<List<Theme>> {
return flowOf(getBuiltInThemes()).combine(customTheme) {
builtIn, custom ->
builtIn + custom
}
}
fun getTheme(id: UUID): Flow<Theme?> {
if (id == DefaultThemeId) return flowOf(getDefaultTheme())
return customTheme
}
fun createTheme(theme: Theme) {
}
fun updateTheme(theme: Theme) {
Log.d("MM20", "updateTheme: $theme")
customTheme.value = theme
}
fun getThemeOrDefault(id: UUID): Flow<Theme> {
return getTheme(id).map { it ?: getDefaultTheme() }
}
private fun getBuiltInThemes(): List<Theme> {
return listOf(
getDefaultTheme(),
)
}
fun getDefaultTheme(): Theme {
return Theme(
id = DefaultThemeId,
builtIn = true,
name = context.getString(R.string.preference_colors_default),
corePalette = EmptyCorePalette,
lightColorScheme = DefaultLightColorScheme,
darkColorScheme = DefaultDarkColorScheme,
)
}
}