From b0db1707a523f7b2284348ff34982600133cc9b0 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Mon, 21 Aug 2023 22:11:31 +0200 Subject: [PATCH] Custom color schemes v2 - Part 1 Add data structures and theme editor --- app/app/build.gradle.kts | 1 + .../de/mm20/launcher2/LauncherApplication.kt | 2 + app/ui/build.gradle.kts | 1 + .../ui/component/BottomSheetDialog.kt | 24 +- .../component/colorpicker/HctColorPicker.kt | 347 +++ .../{ColorPicker.kt => HsvColorPicker.kt} | 12 +- .../component/preferences/ColorPreference.kt | 9 +- .../java/de/mm20/launcher2/ui/ktx/Color.kt | 6 + .../launcher2/ui/settings/SettingsActivity.kt | 16 + .../appearance/AppearanceSettingsScreen.kt | 12 + .../colorscheme/ThemeSettingsScreen.kt | 1856 +++++++++++++++++ .../colorscheme/ThemesSettingsScreen.kt | 134 ++ .../colorscheme/ThemesSettingsScreenVM.kt | 29 + .../mm20/launcher2/ui/theme/LauncherTheme.kt | 37 +- .../launcher2/ui/theme/colorscheme/Custom.kt | 100 +- core/i18n/src/main/res/values/strings.xml | 1 + data/themes/.gitignore | 1 + data/themes/build.gradle.kts | 50 + data/themes/consumer-rules.pro | 0 data/themes/proguard-rules.pro | 21 + data/themes/src/main/AndroidManifest.xml | 1 + .../de/mm20/launcher2/themes/DefaultThemes.kt | 84 + .../java/de/mm20/launcher2/themes/Module.kt | 7 + .../java/de/mm20/launcher2/themes/Theme.kt | 186 ++ .../mm20/launcher2/themes/ThemeRepository.kt | 65 + 25 files changed, 2949 insertions(+), 53 deletions(-) create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/component/colorpicker/HctColorPicker.kt rename app/ui/src/main/java/de/mm20/launcher2/ui/component/colorpicker/{ColorPicker.kt => HsvColorPicker.kt} (96%) create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeSettingsScreen.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreen.kt create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreenVM.kt create mode 100644 data/themes/.gitignore create mode 100644 data/themes/build.gradle.kts create mode 100644 data/themes/consumer-rules.pro create mode 100644 data/themes/proguard-rules.pro create mode 100644 data/themes/src/main/AndroidManifest.xml create mode 100644 data/themes/src/main/java/de/mm20/launcher2/themes/DefaultThemes.kt create mode 100644 data/themes/src/main/java/de/mm20/launcher2/themes/Module.kt create mode 100644 data/themes/src/main/java/de/mm20/launcher2/themes/Theme.kt create mode 100644 data/themes/src/main/java/de/mm20/launcher2/themes/ThemeRepository.kt diff --git a/app/app/build.gradle.kts b/app/app/build.gradle.kts index 84ab76a1..25b7d734 100644 --- a/app/app/build.gradle.kts +++ b/app/app/build.gradle.kts @@ -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")) diff --git a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt index 32f85766..c4f9d149 100644 --- a/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt +++ b/app/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt @@ -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, diff --git a/app/ui/build.gradle.kts b/app/ui/build.gradle.kts index 3d41646f..37e80d0d 100644 --- a/app/ui/build.gradle.kts +++ b/app/ui/build.gradle.kts @@ -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")) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheetDialog.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheetDialog.kt index ab43d386..30445796 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheetDialog.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/BottomSheetDialog.kt @@ -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(), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/colorpicker/HctColorPicker.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/colorpicker/HctColorPicker.kt new file mode 100644 index 00000000..36c1049c --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/colorpicker/HctColorPicker.kt @@ -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 = "#", + ) + } + ) + } + +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/colorpicker/ColorPicker.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/colorpicker/HsvColorPicker.kt similarity index 96% rename from app/ui/src/main/java/de/mm20/launcher2/ui/component/colorpicker/ColorPicker.kt rename to app/ui/src/main/java/de/mm20/launcher2/ui/component/colorpicker/HsvColorPicker.kt index b3ecc98b..d4e52759 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/colorpicker/ColorPicker.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/colorpicker/HsvColorPicker.kt @@ -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) { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/ColorPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/ColorPreference.kt index 82965489..287fb230 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/ColorPreference.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/ColorPreference.kt @@ -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 = { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/Color.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/Color.kt index daeed882..42a4aa3d 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/Color.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/ktx/Color.kt @@ -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()) } \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt index 0266e303..69f1c249 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt @@ -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() } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt index 8ab66178..510d14e5 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/appearance/AppearanceSettingsScreen.kt @@ -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), diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeSettingsScreen.kt new file mode 100644 index 00000000..13256067 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeSettingsScreen.kt @@ -0,0 +1,1856 @@ +package de.mm20.launcher2.ui.settings.colorscheme + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.FlowRowScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AutoFixHigh +import androidx.compose.material.icons.rounded.CheckCircle +import androidx.compose.material.icons.rounded.Colorize +import androidx.compose.material.icons.rounded.DarkMode +import androidx.compose.material.icons.rounded.Edit +import androidx.compose.material.icons.rounded.LightMode +import androidx.compose.material.icons.rounded.Palette +import androidx.compose.material.icons.rounded.PlayArrow +import androidx.compose.material.icons.rounded.RestartAlt +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material.icons.rounded.SettingsSuggest +import androidx.compose.material.icons.rounded.Tag +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.PlainTooltipBox +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Slider +import androidx.compose.material3.Snackbar +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +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.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.viewmodel.compose.viewModel +import de.mm20.launcher2.icons.ColorLayer +import de.mm20.launcher2.icons.StaticLauncherIcon +import de.mm20.launcher2.icons.TintedIconLayer +import de.mm20.launcher2.themes.ColorRef +import de.mm20.launcher2.themes.CorePaletteColor +import de.mm20.launcher2.themes.DefaultDarkColorScheme +import de.mm20.launcher2.themes.DefaultLightColorScheme +import de.mm20.launcher2.themes.FullCorePalette +import de.mm20.launcher2.themes.StaticColor +import de.mm20.launcher2.themes.atTone +import de.mm20.launcher2.themes.get +import de.mm20.launcher2.themes.merge +import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.BottomSheetDialog +import de.mm20.launcher2.ui.component.ShapedLauncherIcon +import de.mm20.launcher2.ui.component.colorpicker.HctColorPicker +import de.mm20.launcher2.ui.component.colorpicker.rememberHctColorPickerState +import de.mm20.launcher2.ui.component.preferences.PreferenceScreen +import de.mm20.launcher2.ui.component.preferences.SwitchPreference +import de.mm20.launcher2.ui.ktx.hct +import de.mm20.launcher2.ui.locals.LocalDarkTheme +import de.mm20.launcher2.ui.theme.colorscheme.darkColorSchemeOf +import de.mm20.launcher2.ui.theme.colorscheme.lightColorSchemeOf +import de.mm20.launcher2.ui.theme.colorscheme.systemCorePalette +import hct.Hct +import palettes.CorePalette +import java.util.UUID +import kotlin.math.roundToInt +import de.mm20.launcher2.themes.Color as ThemeColor + +@Composable +fun ThemeSettingsScreen(themeId: UUID) { + val viewModel: ThemesSettingsScreenVM = viewModel() + + val context = LocalContext.current + val dark = LocalDarkTheme.current + + val theme by remember( + viewModel, + themeId + ) { viewModel.getTheme(themeId) }.collectAsStateWithLifecycle(null) + + var previewDarkTheme by remember(dark) { mutableStateOf(dark) } + val previewColorScheme = + theme?.let { if (previewDarkTheme) darkColorSchemeOf(it) else lightColorSchemeOf(it) } + + val systemPalette = systemCorePalette() + + val mergedCorePalette by remember(theme?.corePalette, systemPalette) { + derivedStateOf { + theme?.corePalette?.merge(systemPalette) ?: systemPalette + } + } + + PreferenceScreen( + title = theme?.name ?: "", + helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/customization/color-schemes", + ) { + if (theme == null || previewColorScheme == null) return@PreferenceScreen + val selectedColorScheme = + if (previewDarkTheme) theme!!.darkColorScheme else theme!!.lightColorScheme + val selectedDefaultScheme = + if (previewDarkTheme) DefaultDarkColorScheme else DefaultLightColorScheme + item { + + Column( + modifier = Modifier + ) { + Text( + stringResource(R.string.preference_custom_colors_corepalette), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.padding(horizontal = 16.dp) + ) + Row( + modifier = Modifier + .horizontalScroll(rememberScrollState()) + .padding(16.dp) + ) { + CorePaletteColorPreference( + title = "Primary", + value = theme?.corePalette?.primary, + onValueChange = { + viewModel.updateTheme( + theme!!.copy( + corePalette = theme!!.corePalette.copy( + primary = it + ) + ) + ) + }, + defaultValue = systemPalette.primary, + modifier = Modifier.padding(end = 12.dp), + ) + CorePaletteColorPreference( + title = "Secondary", + value = theme?.corePalette?.secondary, + onValueChange = { + viewModel.updateTheme( + theme!!.copy( + corePalette = theme!!.corePalette.copy( + secondary = it + ) + ) + ) + }, + defaultValue = systemPalette.secondary, + modifier = Modifier.padding(end = 12.dp), + autoGenerate = { + theme!!.corePalette.primary?.let { + CorePalette.of(it).a2.keyColor.toInt() + } + }, + ) + CorePaletteColorPreference( + title = "Tertiary", + value = theme?.corePalette?.tertiary, + onValueChange = { + viewModel.updateTheme( + theme!!.copy( + corePalette = theme!!.corePalette.copy( + tertiary = it + ) + ) + ) + }, + defaultValue = systemPalette.tertiary, + modifier = Modifier.padding(end = 12.dp), + autoGenerate = { + theme!!.corePalette.primary?.let { + CorePalette.of(it).a3.keyColor.toInt() + } + }, + ) + CorePaletteColorPreference( + title = "Neutral", + value = theme?.corePalette?.neutral, + onValueChange = { + viewModel.updateTheme( + theme!!.copy( + corePalette = theme!!.corePalette.copy( + neutral = it + ) + ) + ) + }, + defaultValue = systemPalette.neutral, + modifier = Modifier.padding(end = 12.dp), + autoGenerate = { + theme!!.corePalette.primary?.let { + CorePalette.of(it).n1.keyColor.toInt() + } + }, + ) + CorePaletteColorPreference( + title = "NeutralVariant", + value = theme?.corePalette?.neutralVariant, + onValueChange = { + viewModel.updateTheme( + theme!!.copy( + corePalette = theme!!.corePalette.copy( + neutralVariant = it + ) + ) + ) + }, + defaultValue = systemPalette.neutralVariant, + modifier = Modifier.padding(end = 12.dp), + autoGenerate = { + theme!!.corePalette.primary?.let { + CorePalette.of(it).n2.keyColor.toInt() + } + }, + ) + CorePaletteColorPreference( + title = "Error", + value = theme?.corePalette?.error, + onValueChange = { + viewModel.updateTheme( + theme!!.copy( + corePalette = theme!!.corePalette.copy( + error = it + ) + ) + ) + }, + defaultValue = systemPalette.error, + autoGenerate = { + theme!!.corePalette.primary?.let { + CorePalette.of(it).error.keyColor.toInt() + } + }, + ) + } + HorizontalDivider() + } + } + item { + ThemePreferenceCategory( + title = "Primary colors", + previewColorScheme = previewColorScheme, + darkMode = previewDarkTheme, + onDarkModeChanged = { previewDarkTheme = it }, + colorPreferences = { + ThemeColorPreference( + title = "Primary", + value = selectedColorScheme.primary, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + primary = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + primary = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.primary, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "On Primary", + value = selectedColorScheme.onPrimary, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + onPrimary = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + onPrimary = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.onPrimary, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "Primary Container", + value = selectedColorScheme.primaryContainer, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + primaryContainer = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + primaryContainer = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.primaryContainer, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "On Primary Container", + value = selectedColorScheme.onPrimaryContainer, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + onPrimaryContainer = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + onPrimaryContainer = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.onPrimaryContainer, + modifier = Modifier.padding(end = 12.dp), + ) + + }, + ) { + Button( + modifier = Modifier + .padding(end = 16.dp) + .align(Alignment.CenterVertically), + onClick = { }) { + Text("Button") + } + Switch( + modifier = Modifier + .padding(end = 16.dp) + .align(Alignment.CenterVertically), + checked = true, + onCheckedChange = {} + ) + FloatingActionButton( + modifier = Modifier + .padding(end = 16.dp) + .align(Alignment.CenterVertically), + onClick = { } + ) { + Icon(Icons.Rounded.Edit, null) + } + ShapedLauncherIcon( + size = 48.dp, + icon = { + StaticLauncherIcon( + foregroundLayer = TintedIconLayer( + ContextCompat.getDrawable( + context, + R.drawable.ic_launcher_monochrome + )!!, + scale = 1.5f, + ), + backgroundLayer = ColorLayer(), + ) + } + ) + } + } + item { + ThemePreferenceCategory( + title = "Secondary colors", + previewColorScheme = previewColorScheme, + darkMode = previewDarkTheme, + onDarkModeChanged = { previewDarkTheme = it }, + colorPreferences = { + ThemeColorPreference( + title = "Secondary", + value = selectedColorScheme.secondary, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + secondary = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + secondary = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.secondary, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "On Secondary", + value = selectedColorScheme.onSecondary, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + onSecondary = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + onSecondary = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.onSecondary, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "Secondary Container", + value = selectedColorScheme.secondaryContainer, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + secondaryContainer = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + secondaryContainer = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.secondaryContainer, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "On Secondary Container", + value = selectedColorScheme.onSecondaryContainer, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + onSecondaryContainer = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + onSecondaryContainer = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.onSecondaryContainer, + modifier = Modifier.padding(end = 12.dp), + ) + }, + ) { + Text( + "Headline", + modifier = Modifier + .padding(end = 16.dp) + .align(Alignment.CenterVertically), + color = MaterialTheme.colorScheme.secondary, + style = MaterialTheme.typography.titleSmall, + ) + FilterChip( + modifier = Modifier + .padding(end = 16.dp) + .align(Alignment.CenterVertically), + label = { Text("Tag") }, + leadingIcon = { + Icon( + Icons.Rounded.Tag, + contentDescription = null, + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + }, + selected = true, + onClick = {}, + ) + FilledTonalIconButton( + modifier = Modifier + .padding(end = 16.dp) + .align(Alignment.CenterVertically), + onClick = { }, + ) { + Icon(Icons.Rounded.PlayArrow, null) + } + } + } + item { + ThemePreferenceCategory( + title = "Tertiary colors", + previewColorScheme = previewColorScheme, + darkMode = previewDarkTheme, + onDarkModeChanged = { previewDarkTheme = it }, + colorPreferences = { + ThemeColorPreference( + title = "Tertiary", + value = selectedColorScheme.tertiary, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + tertiary = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + tertiary = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.tertiary, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "On Tertiary", + value = selectedColorScheme.onTertiary, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + onTertiary = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + onTertiary = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.onTertiary, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "Tertiary Container", + value = selectedColorScheme.tertiaryContainer, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + tertiaryContainer = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + tertiaryContainer = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.tertiaryContainer, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "On Tertiary Container", + value = selectedColorScheme.onTertiaryContainer, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + onTertiaryContainer = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + onTertiaryContainer = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.onTertiaryContainer, + modifier = Modifier.padding(end = 12.dp), + ) + }, + ) { + } + } + item { + ThemePreferenceCategory( + title = "Surface colors", + previewColorScheme = previewColorScheme, + darkMode = previewDarkTheme, + onDarkModeChanged = { previewDarkTheme = it }, + colorPreferences = { + ThemeColorPreference( + title = "Surface Dim", + value = selectedColorScheme.surfaceDim, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + surfaceDim = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + surfaceDim = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.surfaceDim, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "Surface", + value = selectedColorScheme.surface, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + surface = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + surface = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.surface, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "Surface Bright", + value = selectedColorScheme.surfaceBright, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + surfaceBright = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + surfaceBright = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.surfaceBright, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "Surface Tint", + value = selectedColorScheme.surfaceTint, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + surfaceTint = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + surfaceTint = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.surfaceTint, + modifier = Modifier.padding(end = 12.dp), + ) + + ThemeColorPreference( + title = "On Surface", + value = selectedColorScheme.onSurface, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + onSurface = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + onSurface = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.onSurface, + modifier = Modifier.padding(end = 12.dp), + ) + }, + ) { + ElevatedCard( + modifier = Modifier + .padding(end = 16.dp) + .size(64.dp), + elevation = CardDefaults.elevatedCardElevation( + defaultElevation = 0.dp + ) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + "Text", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + ElevatedCard( + modifier = Modifier + .padding(end = 16.dp) + .size(64.dp), + elevation = CardDefaults.elevatedCardElevation( + defaultElevation = 1.dp + ) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Text( + "Text", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + ElevatedCard( + modifier = Modifier + .padding(end = 16.dp) + .size(64.dp), + elevation = CardDefaults.elevatedCardElevation( + defaultElevation = 8.dp + ) + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + Icon(Icons.Rounded.Search, null, tint = MaterialTheme.colorScheme.onSurface) + } + } + } + } + item { + ThemePreferenceCategory( + title = "Surface container colors", + previewColorScheme = previewColorScheme, + darkMode = previewDarkTheme, + onDarkModeChanged = { previewDarkTheme = it }, + colorPreferences = { + ThemeColorPreference( + title = "Surface Container Lowest", + value = selectedColorScheme.surfaceContainerLowest, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + surfaceContainerLowest = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + surfaceContainerLowest = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.surfaceContainerLowest, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "Surface Container Low", + value = selectedColorScheme.surfaceContainerLow, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + surfaceContainerLow = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + surfaceContainerLow = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.surfaceContainerLow, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "Surface Container", + value = selectedColorScheme.surfaceContainer, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + surfaceContainer = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + surfaceContainer = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.surfaceContainer, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "Surface Container High", + value = selectedColorScheme.surfaceContainerHigh, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + surfaceContainerHigh = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + surfaceContainerHigh = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.surfaceContainerHigh, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "Surface Container Highest", + value = selectedColorScheme.surfaceContainerHighest, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + surfaceContainerHighest = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + surfaceContainerHighest = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.surfaceContainerHighest, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "On Surface Variant", + value = selectedColorScheme.surfaceVariant, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + surfaceVariant = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + surfaceVariant = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.surfaceVariant, + modifier = Modifier.padding(end = 12.dp), + ) + }, + ) { + + } + } + item { + ThemePreferenceCategory( + title = "Outline colors", + previewColorScheme = previewColorScheme, + darkMode = previewDarkTheme, + onDarkModeChanged = { previewDarkTheme = it }, + colorPreferences = { + ThemeColorPreference( + title = "Outline", + value = selectedColorScheme.outline, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + outline = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + outline = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.outline, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "Outline Variant", + value = selectedColorScheme.outlineVariant, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + outlineVariant = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + outlineVariant = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.outlineVariant, + modifier = Modifier.padding(end = 12.dp), + ) + }, + ) { + FilterChip( + modifier = Modifier + .padding(end = 16.dp) + .align(Alignment.CenterVertically), + label = { Text("Tag") }, + leadingIcon = { + Icon( + Icons.Rounded.Tag, + contentDescription = null, + modifier = Modifier.size(FilterChipDefaults.IconSize) + ) + }, + selected = false, + onClick = {}, + ) + OutlinedButton( + modifier = Modifier.padding(end = 16.dp) + .align(Alignment.CenterVertically), + onClick = { }) { + Text("Button") + } + OutlinedCard( + modifier = Modifier + .padding(end = 16.dp) + .size(64.dp) + .align(Alignment.CenterVertically) + ) {} + VerticalDivider( + modifier = Modifier.padding(end = 16.dp), + ) + } + } + item { + ThemePreferenceCategory( + title = "Error colors", + previewColorScheme = previewColorScheme, + darkMode = previewDarkTheme, + onDarkModeChanged = { previewDarkTheme = it }, + colorPreferences = { + ThemeColorPreference( + title = "Error", + value = selectedColorScheme.error, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + error = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + error = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.error, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "On Error", + value = selectedColorScheme.onError, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + onError = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + onError = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.onError, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "Error Container", + value = selectedColorScheme.errorContainer, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + errorContainer = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + errorContainer = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.errorContainer, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "On Error Container", + value = selectedColorScheme.onErrorContainer, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + onErrorContainer = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + onErrorContainer = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.onErrorContainer, + modifier = Modifier.padding(end = 12.dp), + ) + }, + ) { + OutlinedTextField( + value = "", + onValueChange = {}, + isError = true, + readOnly = true, + label = {Text("Error")} + ) + } + } + item { + ThemePreferenceCategory( + title = "Inverse colors", + previewColorScheme = previewColorScheme, + darkMode = previewDarkTheme, + onDarkModeChanged = { previewDarkTheme = it }, + colorPreferences = { + ThemeColorPreference( + title = "Inverse Surface", + value = selectedColorScheme.inverseSurface, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + inverseSurface = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + inverseSurface = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.inverseSurface, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "Inverse Surface", + value = selectedColorScheme.inverseOnSurface, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + inverseOnSurface = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + inverseOnSurface = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.inverseOnSurface, + modifier = Modifier.padding(end = 12.dp), + ) + ThemeColorPreference( + title = "Inverse Primary", + value = selectedColorScheme.inversePrimary, + corePalette = mergedCorePalette, + onValueChange = { + viewModel.updateTheme( + if (previewDarkTheme) { + theme!!.copy( + darkColorScheme = theme!!.darkColorScheme.copy( + inversePrimary = it + ) + ) + } else { + theme!!.copy( + lightColorScheme = theme!!.lightColorScheme.copy( + inversePrimary = it + ) + ) + } + ) + }, + defaultValue = selectedDefaultScheme.inversePrimary, + modifier = Modifier.padding(end = 12.dp), + ) + }, + ) { + Snackbar( + action = { + TextButton( + colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.inversePrimary), + onClick = { }, + content = { Text("Action") } + ) + }, + content = { + Text("Snackbar") + } + ) + } + } + } +} + +@Composable +fun CorePaletteColorPreference( + title: String, + value: Int?, + onValueChange: (Int?) -> Unit, + defaultValue: Int, + modifier: Modifier = Modifier, + autoGenerate: (() -> Int?)? = null, +) { + var showDialog by remember { mutableStateOf(false) } + + PlainTooltipBox(tooltip = { Text(title) }) { + ColorSwatch( + color = Color(value ?: defaultValue), + modifier = modifier + .size(48.dp) + .tooltipTrigger() + .clickable { showDialog = true }, + ) + } + + if (showDialog) { + BottomSheetDialog(onDismissRequest = { showDialog = false }) { + Column( + modifier = Modifier.padding(it) + ) { + SwitchPreference( + icon = Icons.Rounded.SettingsSuggest, + title = "Use system default", + value = value == null, + onValueChanged = { + onValueChange(if (it) null else defaultValue) + } + ) + AnimatedVisibility( + value != null, + enter = expandVertically( + expandFrom = Alignment.Top, + ), + exit = shrinkVertically( + shrinkTowards = Alignment.Top, + ) + ) { + Column { + HorizontalDivider( + modifier = Modifier.padding(bottom = 24.dp) + ) + val colorPickerState = rememberHctColorPickerState( + initialColor = Color(value ?: defaultValue), + onColorChanged = { + onValueChange(it.toArgb()) + } + ) + HctColorPicker( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally), + state = colorPickerState, + ) + + if (autoGenerate != null) { + HorizontalDivider( + modifier = Modifier.padding(top = 16.dp) + ) + + TextButton( + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.End), + contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, + onClick = { + val autoGenerated = autoGenerate() + onValueChange(autoGenerated) + if (autoGenerated != null) { + colorPickerState.setColor(Color(autoGenerated)) + } + } + ) { + Icon( + Icons.Rounded.AutoFixHigh, null, + modifier = Modifier + .padding(ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize) + ) + Text("From primary color") + } + } + } + } + } + } + } +} + +@Composable +fun ThemePreferenceCategory( + title: String, + previewColorScheme: ColorScheme, + darkMode: Boolean, + onDarkModeChanged: (Boolean) -> Unit, + colorPreferences: @Composable () -> Unit = {}, + preview: @Composable FlowRowScope.() -> Unit +) { + Column( + modifier = Modifier.padding(top = 16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + title, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.weight(1f) + ) + SingleChoiceSegmentedButtonRow { + SegmentedButton( + shape = SegmentedButtonDefaults.shape(position = 0, count = 2), + selected = !darkMode, + onClick = { onDarkModeChanged(false) } + ) { + Icon(Icons.Rounded.LightMode, null) + } + SegmentedButton( + shape = SegmentedButtonDefaults.shape(position = 1, count = 2), + selected = darkMode, + onClick = { onDarkModeChanged(true) } + ) { + Icon(Icons.Rounded.DarkMode, null) + } + } + } + MaterialTheme( + colorScheme = previewColorScheme + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow + ) + ) { + + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + ) { + preview() + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(16.dp) + ) { + colorPreferences() + } + HorizontalDivider() + } + +} + +@Composable +fun ThemeColorPreference( + title: String, + value: ThemeColor?, + corePalette: FullCorePalette, + onValueChange: (ThemeColor?) -> Unit, + defaultValue: ThemeColor, + modifier: Modifier = Modifier, +) { + var showDialog by remember { mutableStateOf(false) } + + val actualValue = value ?: defaultValue + + PlainTooltipBox(tooltip = { Text(title) }) { + ColorSwatch( + color = Color(actualValue.get(corePalette)), + modifier = modifier + .size(48.dp) + .tooltipTrigger() + .clickable { showDialog = true }, + ) + } + + if (showDialog) { + BottomSheetDialog(onDismissRequest = { showDialog = false }) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(it), + ) { + SingleChoiceSegmentedButtonRow( + modifier = Modifier.fillMaxWidth() + ) { + SegmentedButton( + selected = actualValue is ColorRef, + onClick = { + if (actualValue is ColorRef) return@SegmentedButton + onValueChange(defaultValue) + }, + icon = { + SegmentedButtonDefaults.SegmentedButtonIcon( + active = actualValue is ColorRef, + ) { + Icon( + Icons.Rounded.Palette, + null, + modifier = Modifier + .size(SegmentedButtonDefaults.IconSize) + ) + } + }, + shape = SegmentedButtonDefaults.shape(position = 0, count = 2) + ) { + Text("From palette") + } + SegmentedButton( + selected = actualValue is StaticColor, + onClick = { + onValueChange(StaticColor(actualValue.get(corePalette))) + }, + icon = { + SegmentedButtonDefaults.SegmentedButtonIcon( + active = actualValue is StaticColor, + ) { + Icon( + Icons.Rounded.Colorize, + null, + modifier = Modifier + .size(SegmentedButtonDefaults.IconSize) + ) + } + }, + shape = SegmentedButtonDefaults.shape(position = 1, count = 2) + ) { + Text("Custom") + } + } + AnimatedContent( + actualValue, + label = "AnimatedContent", + contentKey = { it is StaticColor } + ) { themeColor -> + Column { + if (themeColor is StaticColor) { + val colorPickerState = rememberHctColorPickerState( + initialColor = Color(themeColor.color), + onColorChanged = { + onValueChange(StaticColor(it.toArgb())) + } + ) + HctColorPicker( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp) + .align(Alignment.CenterHorizontally), + state = colorPickerState + ) + } else if (themeColor is ColorRef) { + val hct = Hct.fromInt(corePalette.get(themeColor.color)) + val hue = hct.hue.toFloat() + val chroma = hct.chroma.toFloat() + var tone by remember(value == null) { mutableStateOf(themeColor.tone.toFloat()) } + Row( + modifier = Modifier.padding(top = 24.dp, bottom = 8.dp) + ) { + ColorSwatch( + color = Color( + corePalette + .get(CorePaletteColor.Primary) + .atTone(tone.toInt()) + ), + modifier = Modifier + .padding(8.dp) + .size(64.dp) + .clickable { + onValueChange( + ColorRef( + CorePaletteColor.Primary, + tone.roundToInt() + ) + ) + }, + selected = themeColor.color == CorePaletteColor.Primary, + ) + Spacer(modifier = Modifier.weight(1f)) + ColorSwatch( + color = Color( + corePalette + .get(CorePaletteColor.Secondary) + .atTone(tone.toInt()) + ), + modifier = Modifier + .padding(8.dp) + .size(64.dp) + .clickable { + onValueChange( + ColorRef( + CorePaletteColor.Secondary, + tone.roundToInt() + ) + ) + }, + selected = themeColor.color == CorePaletteColor.Secondary, + ) + Spacer(modifier = Modifier.weight(1f)) + ColorSwatch( + color = Color( + corePalette + .get(CorePaletteColor.Tertiary) + .atTone(tone.toInt()) + ), + modifier = Modifier + .padding(8.dp) + .size(64.dp) + .clickable { + onValueChange( + ColorRef( + CorePaletteColor.Tertiary, + tone.roundToInt() + ) + ) + }, + selected = themeColor.color == CorePaletteColor.Tertiary, + ) + } + Row( + modifier = Modifier.padding(bottom = 16.dp) + ) { + ColorSwatch( + color = Color( + corePalette + .get(CorePaletteColor.Neutral) + .atTone(tone.toInt()) + ), + modifier = Modifier + .padding(8.dp) + .size(64.dp) + .clickable { + onValueChange( + ColorRef( + CorePaletteColor.Neutral, + tone.roundToInt() + ) + ) + }, + selected = themeColor.color == CorePaletteColor.Neutral, + ) + Spacer(modifier = Modifier.weight(1f)) + ColorSwatch( + color = Color( + corePalette + .get(CorePaletteColor.NeutralVariant) + .atTone(tone.toInt()) + ), + modifier = Modifier + .padding(8.dp) + .size(64.dp) + .clickable { + onValueChange( + ColorRef( + CorePaletteColor.NeutralVariant, + tone.roundToInt() + ) + ) + }, + selected = themeColor.color == CorePaletteColor.NeutralVariant, + ) + Spacer(modifier = Modifier.weight(1f)) + ColorSwatch( + color = Color( + corePalette + .get(CorePaletteColor.Error) + .atTone(tone.toInt()) + ), + modifier = Modifier + .padding(8.dp) + .size(64.dp) + .clickable { + onValueChange( + ColorRef( + CorePaletteColor.Error, + tone.roundToInt() + ) + ) + }, + selected = themeColor.color == CorePaletteColor.Error, + ) + } + 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 = tone, + valueRange = 0f..100f, + onValueChange = { + tone = it + onValueChange(themeColor.copy(tone = it.roundToInt())) + }, + track = { + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + ) { + drawRoundRect( + brush = Brush.horizontalGradient( + colors = listOf( + Color.hct(hue, chroma, 0f), + Color.hct(hue, chroma, 10f), + Color.hct(hue, chroma, 20f), + Color.hct(hue, chroma, 30f), + Color.hct(hue, chroma, 40f), + Color.hct(hue, chroma, 50f), + Color.hct(hue, chroma, 60f), + Color.hct(hue, chroma, 70f), + Color.hct(hue, chroma, 80f), + Color.hct(hue, chroma, 90f), + Color.hct(hue, 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 = tone.roundToInt().toString(), + modifier = Modifier.width(32.dp), + style = MaterialTheme.typography.labelMedium, + textAlign = TextAlign.Center, + ) + } + } + HorizontalDivider( + modifier = Modifier.padding(top = 16.dp) + ) + + TextButton( + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.End), + contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, + onClick = { onValueChange(null) } + ) { + Icon( + Icons.Rounded.RestartAlt, null, + modifier = Modifier + .padding(ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize) + ) + Text("Restore default") + } + } + } + } + } + } +} + +@Composable +fun ColorSwatch( + color: Color, + modifier: Modifier = Modifier, + selected: Boolean = false +) { + val darkTheme = LocalDarkTheme.current + val iconColor = Color(Hct.fromInt(color.toArgb()).let { + val tone = if (darkTheme) { + if (it.tone.toInt() > 40) 30f + else 60f + } else { + if (it.tone.toInt() < 60) 80f + else 40f + } + it.apply { + this.tone = tone.toDouble() + }.toInt() + }) + val borderColor = Color(Hct.fromInt(color.toArgb()).let { + val tone = if (darkTheme) 30f else 80f + it.apply { + this.tone = tone.toDouble() + }.toInt() + }) + Box( + modifier = modifier + .clip(CircleShape) + .border( + if (selected) 4.dp else 1.dp, + borderColor, + CircleShape + ) + .background(color), + contentAlignment = Alignment.Center + ) { + if (selected) { + Icon( + Icons.Rounded.CheckCircle, + null, + modifier = Modifier.size(32.dp), + tint = iconColor, + ) + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreen.kt new file mode 100644 index 00000000..ab7ab34e --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreen.kt @@ -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() + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreenVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreenVM.kt new file mode 100644 index 00000000..c98d8e59 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemesSettingsScreenVM.kt @@ -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 = flowOf(DefaultThemeId) + val themes: Flow> = themeRepository.getThemes() + + fun getTheme(id: UUID): Flow { + return themeRepository.getTheme(id) + } + + fun updateTheme(theme: Theme) { + Log.d("MM20", "updateTheme: $theme") + themeRepository.updateTheme(theme) + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/LauncherTheme.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/LauncherTheme.kt index 457b94be..9525271a 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/LauncherTheme.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/LauncherTheme.kt @@ -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 { - 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) } - } } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/colorscheme/Custom.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/colorscheme/Custom.kt index 1b3b00bc..0de0bbb1 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/theme/colorscheme/Custom.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/theme/colorscheme/Custom.kt @@ -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 { + 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), diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml index 3d798876..e6eae94e 100644 --- a/core/i18n/src/main/res/values/strings.xml +++ b/core/i18n/src/main/res/values/strings.xml @@ -436,6 +436,7 @@ Neutral Neutral Variant Error + Color palette Advanced mode Simple mode Generate from primary color diff --git a/data/themes/.gitignore b/data/themes/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/data/themes/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/data/themes/build.gradle.kts b/data/themes/build.gradle.kts new file mode 100644 index 00000000..bf1f844d --- /dev/null +++ b/data/themes/build.gradle.kts @@ -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")) + +} diff --git a/data/themes/consumer-rules.pro b/data/themes/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/data/themes/proguard-rules.pro b/data/themes/proguard-rules.pro new file mode 100644 index 00000000..6f610ec0 --- /dev/null +++ b/data/themes/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/data/themes/src/main/AndroidManifest.xml b/data/themes/src/main/AndroidManifest.xml new file mode 100644 index 00000000..227314ee --- /dev/null +++ b/data/themes/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/themes/src/main/java/de/mm20/launcher2/themes/DefaultThemes.kt b/data/themes/src/main/java/de/mm20/launcher2/themes/DefaultThemes.kt new file mode 100644 index 00000000..1df544a3 --- /dev/null +++ b/data/themes/src/main/java/de/mm20/launcher2/themes/DefaultThemes.kt @@ -0,0 +1,84 @@ +package de.mm20.launcher2.themes + +import java.util.UUID + + +val DefaultThemeId = UUID(0L, 0L) + +val DefaultLightColorScheme = ColorScheme( + 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( + 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), +) \ No newline at end of file diff --git a/data/themes/src/main/java/de/mm20/launcher2/themes/Module.kt b/data/themes/src/main/java/de/mm20/launcher2/themes/Module.kt new file mode 100644 index 00000000..cc11f784 --- /dev/null +++ b/data/themes/src/main/java/de/mm20/launcher2/themes/Module.kt @@ -0,0 +1,7 @@ +package de.mm20.launcher2.themes + +import org.koin.dsl.module + +val themesModule = module { + factory { ThemeRepository(get()) } +} \ No newline at end of file diff --git a/data/themes/src/main/java/de/mm20/launcher2/themes/Theme.kt b/data/themes/src/main/java/de/mm20/launcher2/themes/Theme.kt new file mode 100644 index 00000000..5f31dfd9 --- /dev/null +++ b/data/themes/src/main/java/de/mm20/launcher2/themes/Theme.kt @@ -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( + val primary: T, + val secondary: T, + val tertiary: T, + val neutral: T, + val neutralVariant: T, + val error: T, +) + +val EmptyCorePalette = CorePalette(null, null, null, null, null, null) + +typealias FullCorePalette = CorePalette +typealias PartialCorePalette = CorePalette + +data class ColorScheme( + 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 +typealias PartialColorScheme = ColorScheme + +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 CorePalette.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, + ) +} \ No newline at end of file diff --git a/data/themes/src/main/java/de/mm20/launcher2/themes/ThemeRepository.kt b/data/themes/src/main/java/de/mm20/launcher2/themes/ThemeRepository.kt new file mode 100644 index 00000000..a9418ea4 --- /dev/null +++ b/data/themes/src/main/java/de/mm20/launcher2/themes/ThemeRepository.kt @@ -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> { + return flowOf(getBuiltInThemes()).combine(customTheme) { + builtIn, custom -> + builtIn + custom + } + } + + fun getTheme(id: UUID): Flow { + 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 { + return getTheme(id).map { it ?: getDefaultTheme() } + } + + private fun getBuiltInThemes(): List { + 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, + ) + } + +} \ No newline at end of file