Custom color schemes v2 - Part 1

Add data structures and theme editor
This commit is contained in:
MM20
2023-08-21 23:43:45 +02:00
parent 411628c607
commit b0db1707a5
25 changed files with 2949 additions and 53 deletions
+1
View File
@@ -0,0 +1 @@
/build
+50
View File
@@ -0,0 +1,50 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "de.mm20.launcher2.themes"
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.bundles.androidx.lifecycle)
implementation(libs.koin.android)
implementation(project(":core:base"))
implementation(project(":core:database"))
implementation(project(":core:crashreporter"))
implementation(project(":libs:material-color-utilities"))
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+1
View File
@@ -0,0 +1 @@
<manifest />
@@ -0,0 +1,84 @@
package de.mm20.launcher2.themes
import java.util.UUID
val DefaultThemeId = UUID(0L, 0L)
val DefaultLightColorScheme = ColorScheme<Color>(
primary = ColorRef(CorePaletteColor.Primary, 40),
onPrimary = ColorRef(CorePaletteColor.Primary, 100),
primaryContainer = ColorRef(CorePaletteColor.Primary, 90),
onPrimaryContainer = ColorRef(CorePaletteColor.Primary, 10),
secondary = ColorRef(CorePaletteColor.Secondary, 40),
onSecondary = ColorRef(CorePaletteColor.Secondary, 100),
secondaryContainer = ColorRef(CorePaletteColor.Secondary, 90),
onSecondaryContainer = ColorRef(CorePaletteColor.Secondary, 10),
tertiary = ColorRef(CorePaletteColor.Tertiary, 40),
onTertiary = ColorRef(CorePaletteColor.Tertiary, 100),
tertiaryContainer = ColorRef(CorePaletteColor.Tertiary, 90),
onTertiaryContainer = ColorRef(CorePaletteColor.Tertiary, 10),
error = ColorRef(CorePaletteColor.Error, 40),
onError = ColorRef(CorePaletteColor.Error, 100),
errorContainer = ColorRef(CorePaletteColor.Error, 90),
onErrorContainer = ColorRef(CorePaletteColor.Error, 10),
surfaceDim = ColorRef(CorePaletteColor.Neutral, 87),
surface = ColorRef(CorePaletteColor.Neutral, 98),
surfaceBright = ColorRef(CorePaletteColor.Neutral, 98),
surfaceContainerLowest = ColorRef(CorePaletteColor.Neutral, 100),
surfaceContainerLow = ColorRef(CorePaletteColor.Neutral, 96),
surfaceContainer = ColorRef(CorePaletteColor.Neutral, 94),
surfaceContainerHigh = ColorRef(CorePaletteColor.Neutral, 92),
surfaceContainerHighest = ColorRef(CorePaletteColor.Neutral, 90),
onSurface = ColorRef(CorePaletteColor.Neutral, 10),
onSurfaceVariant = ColorRef(CorePaletteColor.NeutralVariant, 30),
outline = ColorRef(CorePaletteColor.NeutralVariant, 50),
outlineVariant = ColorRef(CorePaletteColor.NeutralVariant, 80),
inverseSurface = ColorRef(CorePaletteColor.Neutral, 20),
inverseOnSurface = ColorRef(CorePaletteColor.Neutral, 95),
inversePrimary = ColorRef(CorePaletteColor.Primary, 80),
surfaceVariant = ColorRef(CorePaletteColor.NeutralVariant, 90),
surfaceTint = ColorRef(CorePaletteColor.Primary, 40),
background = ColorRef(CorePaletteColor.Neutral, 98),
onBackground = ColorRef(CorePaletteColor.Neutral, 10),
scrim = ColorRef(CorePaletteColor.Neutral, 0),
)
val DefaultDarkColorScheme = ColorScheme<Color>(
primary = ColorRef(CorePaletteColor.Primary, 80),
onPrimary = ColorRef(CorePaletteColor.Primary, 20),
primaryContainer = ColorRef(CorePaletteColor.Primary, 30),
onPrimaryContainer = ColorRef(CorePaletteColor.Primary, 90),
secondary = ColorRef(CorePaletteColor.Secondary, 80),
onSecondary = ColorRef(CorePaletteColor.Secondary, 20),
secondaryContainer = ColorRef(CorePaletteColor.Secondary, 30),
onSecondaryContainer = ColorRef(CorePaletteColor.Secondary, 90),
tertiary = ColorRef(CorePaletteColor.Tertiary, 80),
onTertiary = ColorRef(CorePaletteColor.Tertiary, 20),
tertiaryContainer = ColorRef(CorePaletteColor.Tertiary, 30),
onTertiaryContainer = ColorRef(CorePaletteColor.Tertiary, 90),
error = ColorRef(CorePaletteColor.Error, 80),
onError = ColorRef(CorePaletteColor.Error, 20),
errorContainer = ColorRef(CorePaletteColor.Error, 30),
onErrorContainer = ColorRef(CorePaletteColor.Error, 90),
surfaceDim = ColorRef(CorePaletteColor.Neutral, 6),
surface = ColorRef(CorePaletteColor.Neutral, 6),
surfaceBright = ColorRef(CorePaletteColor.Neutral, 24),
surfaceContainerLowest = ColorRef(CorePaletteColor.Neutral, 4),
surfaceContainerLow = ColorRef(CorePaletteColor.Neutral, 10),
surfaceContainer = ColorRef(CorePaletteColor.Neutral, 12),
surfaceContainerHigh = ColorRef(CorePaletteColor.Neutral, 17),
surfaceContainerHighest = ColorRef(CorePaletteColor.Neutral, 22),
onSurface = ColorRef(CorePaletteColor.Neutral, 90),
onSurfaceVariant = ColorRef(CorePaletteColor.NeutralVariant, 80),
outline = ColorRef(CorePaletteColor.NeutralVariant, 60),
outlineVariant = ColorRef(CorePaletteColor.NeutralVariant, 30),
inverseSurface = ColorRef(CorePaletteColor.Neutral, 98),
inverseOnSurface = ColorRef(CorePaletteColor.Neutral, 10),
inversePrimary = ColorRef(CorePaletteColor.Primary, 40),
surfaceVariant = ColorRef(CorePaletteColor.NeutralVariant, 30),
surfaceTint = ColorRef(CorePaletteColor.Primary, 80),
background = ColorRef(CorePaletteColor.Neutral, 6),
onBackground = ColorRef(CorePaletteColor.Neutral, 90),
scrim = ColorRef(CorePaletteColor.Neutral, 0),
)
@@ -0,0 +1,7 @@
package de.mm20.launcher2.themes
import org.koin.dsl.module
val themesModule = module {
factory { ThemeRepository(get()) }
}
@@ -0,0 +1,186 @@
package de.mm20.launcher2.themes
import hct.Hct
import java.util.UUID
enum class CorePaletteColor {
Primary,
Secondary,
Tertiary,
Neutral,
NeutralVariant,
Error;
override fun toString(): String {
return when (this) {
Primary -> "p"
Secondary -> "s"
Tertiary -> "t"
Neutral -> "n"
NeutralVariant -> "nv"
Error -> "e"
}
}
}
sealed interface Color
data class ColorRef(
val color: CorePaletteColor,
val tone: Int,
) : Color {
override fun toString(): String {
return "\$${color.name}.$tone"
}
}
@JvmInline
value class StaticColor(val color: Int) : Color {
override fun toString(): String {
return "#${color.toString(16).padStart(6, '0')}"
}
}
data class CorePalette<out T : Int?>(
val primary: T,
val secondary: T,
val tertiary: T,
val neutral: T,
val neutralVariant: T,
val error: T,
)
val EmptyCorePalette = CorePalette<Int?>(null, null, null, null, null, null)
typealias FullCorePalette = CorePalette<Int>
typealias PartialCorePalette = CorePalette<Int?>
data class ColorScheme<out T: Color?>(
val primary: T,
val onPrimary: T,
val primaryContainer: T,
val onPrimaryContainer: T,
val secondary: T,
val onSecondary: T,
val secondaryContainer: T,
val onSecondaryContainer: T,
val tertiary: T,
val onTertiary: T,
val tertiaryContainer: T,
val onTertiaryContainer: T,
val error: T,
val onError: T,
val errorContainer: T,
val onErrorContainer: T,
val surface: T,
val onSurface: T,
val onSurfaceVariant: T,
val outline: T,
val outlineVariant: T,
val inverseSurface: T,
val inverseOnSurface: T,
val inversePrimary: T,
val surfaceDim: T,
val surfaceBright: T,
val surfaceContainerLowest: T,
val surfaceContainerLow: T,
val surfaceContainer: T,
val surfaceContainerHigh: T,
val surfaceContainerHighest: T,
val background: T,
val onBackground: T,
val surfaceTint: T,
val scrim: T,
val surfaceVariant: T,
)
typealias FullColorScheme = ColorScheme<Color>
typealias PartialColorScheme = ColorScheme<Color?>
data class Theme(
val id: UUID,
val builtIn: Boolean = false,
val name: String,
val corePalette: PartialCorePalette = EmptyCorePalette,
val lightColorScheme: PartialColorScheme = DefaultLightColorScheme,
val darkColorScheme: PartialColorScheme = DefaultDarkColorScheme,
)
fun <T : Int?> CorePalette<T>.get(color: CorePaletteColor): T {
return when (color) {
CorePaletteColor.Primary -> primary
CorePaletteColor.Secondary -> secondary
CorePaletteColor.Tertiary -> tertiary
CorePaletteColor.Neutral -> neutral
CorePaletteColor.NeutralVariant -> neutralVariant
CorePaletteColor.Error -> error
}
}
fun Color.get(corePalette: FullCorePalette): Int {
return when (this) {
is StaticColor -> color
is ColorRef -> {
corePalette.get(this.color).atTone(this.tone)
}
}
}
fun Int.atTone(tone: Int): Int {
return Hct.fromInt(this).apply {
setTone(tone.toDouble())
}.toInt()
}
fun PartialCorePalette.merge(other: FullCorePalette): FullCorePalette {
return CorePalette(
primary = this.primary ?: other.primary,
secondary = this.secondary ?: other.secondary,
tertiary = this.tertiary ?: other.tertiary,
neutral = this.neutral ?: other.neutral,
neutralVariant = this.neutralVariant ?: other.neutralVariant,
error = this.error ?: other.error,
)
}
fun PartialColorScheme.merge(other: FullColorScheme): FullColorScheme {
return ColorScheme(
primary = this.primary ?: other.primary,
onPrimary = this.onPrimary ?: other.onPrimary,
primaryContainer = this.primaryContainer ?: other.primaryContainer,
onPrimaryContainer = this.onPrimaryContainer ?: other.onPrimaryContainer,
secondary = this.secondary ?: other.secondary,
onSecondary = this.onSecondary ?: other.onSecondary,
secondaryContainer = this.secondaryContainer ?: other.secondaryContainer,
onSecondaryContainer = this.onSecondaryContainer ?: other.onSecondaryContainer,
tertiary = this.tertiary ?: other.tertiary,
onTertiary = this.onTertiary ?: other.onTertiary,
tertiaryContainer = this.tertiaryContainer ?: other.tertiaryContainer,
onTertiaryContainer = this.onTertiaryContainer ?: other.onTertiaryContainer,
error = this.error ?: other.error,
onError = this.onError ?: other.onError,
errorContainer = this.errorContainer ?: other.errorContainer,
onErrorContainer = this.onErrorContainer ?: other.onErrorContainer,
surfaceDim = this.surfaceDim ?: other.surfaceDim,
surface = this.surface ?: other.surface,
surfaceBright = this.surfaceBright ?: other.surfaceBright,
surfaceContainerLowest = this.surfaceContainerLowest ?: other.surfaceContainerLowest,
surfaceContainerLow = this.surfaceContainerLow ?: other.surfaceContainerLow,
surfaceContainer = this.surfaceContainer ?: other.surfaceContainer,
surfaceContainerHigh = this.surfaceContainerHigh ?: other.surfaceContainerHigh,
surfaceContainerHighest = this.surfaceContainerHighest ?: other.surfaceContainerHighest,
onSurface = this.onSurface ?: other.onSurface,
onSurfaceVariant = this.onSurfaceVariant ?: other.onSurfaceVariant,
outline = this.outline ?: other.outline,
outlineVariant = this.outlineVariant ?: other.outlineVariant,
inverseSurface = this.inverseSurface ?: other.inverseSurface,
inverseOnSurface = this.inverseOnSurface ?: other.inverseOnSurface,
inversePrimary = this.inversePrimary ?: other.inversePrimary,
surfaceVariant = this.surfaceVariant ?: other.surfaceVariant,
scrim = this.scrim ?: other.scrim,
onBackground = this.onBackground ?: other.onBackground,
background = this.background ?: other.background,
surfaceTint = this.surfaceTint ?: other.surfaceTint,
)
}
@@ -0,0 +1,65 @@
package de.mm20.launcher2.themes
import android.content.Context
import android.util.Log
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import java.util.UUID
class ThemeRepository(
private val context: Context,
) {
private val customTheme = MutableStateFlow(Theme(
id = UUID.randomUUID(),
builtIn = false,
name = "Custom",
corePalette = EmptyCorePalette,
lightColorScheme = DefaultLightColorScheme,
darkColorScheme = DefaultDarkColorScheme,
))
fun getThemes(): Flow<List<Theme>> {
return flowOf(getBuiltInThemes()).combine(customTheme) {
builtIn, custom ->
builtIn + custom
}
}
fun getTheme(id: UUID): Flow<Theme?> {
if (id == DefaultThemeId) return flowOf(getDefaultTheme())
return customTheme
}
fun createTheme(theme: Theme) {
}
fun updateTheme(theme: Theme) {
Log.d("MM20", "updateTheme: $theme")
customTheme.value = theme
}
fun getThemeOrDefault(id: UUID): Flow<Theme> {
return getTheme(id).map { it ?: getDefaultTheme() }
}
private fun getBuiltInThemes(): List<Theme> {
return listOf(
getDefaultTheme(),
)
}
fun getDefaultTheme(): Theme {
return Theme(
id = DefaultThemeId,
builtIn = true,
name = context.getString(R.string.preference_colors_default),
corePalette = EmptyCorePalette,
lightColorScheme = DefaultLightColorScheme,
darkColorScheme = DefaultDarkColorScheme,
)
}
}