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