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:currencies"))
|
||||||
implementation(project(":data:customattrs"))
|
implementation(project(":data:customattrs"))
|
||||||
implementation(project(":data:searchable"))
|
implementation(project(":data:searchable"))
|
||||||
|
implementation(project(":data:themes"))
|
||||||
implementation(project(":data:files"))
|
implementation(project(":data:files"))
|
||||||
implementation(project(":libs:g-services"))
|
implementation(project(":libs:g-services"))
|
||||||
implementation(project(":core:i18n"))
|
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.favorites.favoritesModule
|
||||||
import de.mm20.launcher2.services.tags.servicesTagsModule
|
import de.mm20.launcher2.services.tags.servicesTagsModule
|
||||||
import de.mm20.launcher2.services.widgets.widgetsServiceModule
|
import de.mm20.launcher2.services.widgets.widgetsServiceModule
|
||||||
|
import de.mm20.launcher2.themes.themesModule
|
||||||
import de.mm20.launcher2.weather.weatherModule
|
import de.mm20.launcher2.weather.weatherModule
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
@ -78,6 +79,7 @@ class LauncherApplication : Application(), CoroutineScope, ImageLoaderFactory {
|
|||||||
preferencesModule,
|
preferencesModule,
|
||||||
searchModule,
|
searchModule,
|
||||||
searchActionsModule,
|
searchActionsModule,
|
||||||
|
themesModule,
|
||||||
unitConverterModule,
|
unitConverterModule,
|
||||||
weatherModule,
|
weatherModule,
|
||||||
websitesModule,
|
websitesModule,
|
||||||
|
|||||||
@ -130,6 +130,7 @@ dependencies {
|
|||||||
implementation(project(":data:files"))
|
implementation(project(":data:files"))
|
||||||
implementation(project(":data:widgets"))
|
implementation(project(":data:widgets"))
|
||||||
implementation(project(":data:searchable"))
|
implementation(project(":data:searchable"))
|
||||||
|
implementation(project(":data:themes"))
|
||||||
implementation(project(":data:wikipedia"))
|
implementation(project(":data:wikipedia"))
|
||||||
implementation(project(":services:badges"))
|
implementation(project(":services:badges"))
|
||||||
implementation(project(":core:crashreporter"))
|
implementation(project(":core:crashreporter"))
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package de.mm20.launcher2.ui.component
|
package de.mm20.launcher2.ui.component
|
||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
import android.util.Log
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.spring
|
import androidx.compose.animation.core.spring
|
||||||
import androidx.compose.foundation.background
|
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.RowScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
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.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeContent
|
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.shape.CornerSize
|
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.BottomSheetDefaults
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.LocalAbsoluteTonalElevation
|
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.unit.dp
|
||||||
import androidx.compose.ui.window.Popup
|
import androidx.compose.ui.window.Popup
|
||||||
import androidx.compose.ui.window.PopupProperties
|
import androidx.compose.ui.window.PopupProperties
|
||||||
import de.mm20.launcher2.ui.ktx.toDp
|
|
||||||
import de.mm20.launcher2.ui.ktx.toPixels
|
import de.mm20.launcher2.ui.ktx.toPixels
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@ -158,6 +150,7 @@ fun BottomSheetDialog(
|
|||||||
LocalAbsoluteTonalElevation provides 0.dp,
|
LocalAbsoluteTonalElevation provides 0.dp,
|
||||||
) {
|
) {
|
||||||
Popup(
|
Popup(
|
||||||
|
alignment = Alignment.BottomCenter,
|
||||||
properties = PopupProperties(
|
properties = PopupProperties(
|
||||||
dismissOnBackPress = dismissible(),
|
dismissOnBackPress = dismissible(),
|
||||||
dismissOnClickOutside = dismissible(),
|
dismissOnClickOutside = dismissible(),
|
||||||
@ -168,8 +161,7 @@ fun BottomSheetDialog(
|
|||||||
) {
|
) {
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize(),
|
||||||
.consumeWindowInsets(WindowInsets.systemBars),
|
|
||||||
propagateMinConstraints = true,
|
propagateMinConstraints = true,
|
||||||
contentAlignment = Alignment.BottomCenter
|
contentAlignment = Alignment.BottomCenter
|
||||||
) {
|
) {
|
||||||
@ -221,7 +213,10 @@ fun BottomSheetDialog(
|
|||||||
draggableState.updateAnchors(
|
draggableState.updateAnchors(
|
||||||
DraggableAnchors {
|
DraggableAnchors {
|
||||||
SwipeState.Dismiss at 0f
|
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)
|
if (hasFullAnchor) SwipeState.Full at -min(maxHeightPx, sheetHeight)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -300,7 +295,8 @@ fun BottomSheetDialog(
|
|||||||
maxHeightPx.toInt() +
|
maxHeightPx.toInt() +
|
||||||
(draggableState.offset
|
(draggableState.offset
|
||||||
.takeIf { !it.isNaN() }
|
.takeIf { !it.isNaN() }
|
||||||
?.roundToInt() ?: 0).coerceAtLeast(heightPx.toInt())
|
?.roundToInt()
|
||||||
|
?: 0).coerceAtLeast(heightPx.toInt())
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.fillMaxWidth(),
|
.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.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -27,7 +26,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.AbsoluteAlignment
|
import androidx.compose.ui.AbsoluteAlignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
@ -43,7 +41,7 @@ import kotlin.math.atan2
|
|||||||
import android.graphics.Color as AndroidColor
|
import android.graphics.Color as AndroidColor
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class ColorPickerState(
|
class HsvColorPickerState(
|
||||||
initialColor: Color,
|
initialColor: Color,
|
||||||
val onColorChanged: (Color) -> Unit,
|
val onColorChanged: (Color) -> Unit,
|
||||||
) {
|
) {
|
||||||
@ -99,15 +97,15 @@ class ColorPickerState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberColorPickerState(initialColor: Color, onColorChanged: (Color) -> Unit): ColorPickerState {
|
fun rememberHsvColorPickerState(initialColor: Color, onColorChanged: (Color) -> Unit): HsvColorPickerState {
|
||||||
return remember(initialColor, onColorChanged) {
|
return remember(initialColor, onColorChanged) {
|
||||||
ColorPickerState(initialColor, onColorChanged)
|
HsvColorPickerState(initialColor, onColorChanged)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ColorPicker(
|
fun HsvColorPicker(
|
||||||
state: ColorPickerState,
|
state: HsvColorPickerState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
@ -8,9 +8,8 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import de.mm20.launcher2.ui.component.colorpicker.ColorPicker
|
import de.mm20.launcher2.ui.component.colorpicker.HsvColorPicker
|
||||||
import de.mm20.launcher2.ui.component.colorpicker.rememberColorPickerState
|
import de.mm20.launcher2.ui.component.colorpicker.rememberHsvColorPickerState
|
||||||
import de.mm20.launcher2.ui.ktx.toHexString
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ColorPreference(
|
fun ColorPreference(
|
||||||
@ -52,10 +51,10 @@ fun ColorPreference(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
val state = rememberColorPickerState(value ?: Color.Black) {
|
val state = rememberHsvColorPickerState(value ?: Color.Black) {
|
||||||
color = it
|
color = it
|
||||||
}
|
}
|
||||||
ColorPicker(state = state)
|
HsvColorPicker(state = state)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package de.mm20.launcher2.ui.ktx
|
package de.mm20.launcher2.ui.ktx
|
||||||
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import hct.Hct
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
fun Color.toHexString(): String {
|
fun Color.toHexString(): String {
|
||||||
@ -12,3 +13,8 @@ fun Color.toHexString(): String {
|
|||||||
green.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 }
|
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.clockwidget.ClockWidgetSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.colorscheme.ColorSchemeSettingsScreen
|
import de.mm20.launcher2.ui.settings.colorscheme.ColorSchemeSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.colorscheme.CustomColorSchemeSettingsScreen
|
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.CrashReportScreen
|
||||||
import de.mm20.launcher2.ui.settings.crashreporter.CrashReporterScreen
|
import de.mm20.launcher2.ui.settings.crashreporter.CrashReporterScreen
|
||||||
import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
|
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 de.mm20.launcher2.ui.theme.wallpaperColorsAsState
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
class SettingsActivity : BaseActivity() {
|
class SettingsActivity : BaseActivity() {
|
||||||
|
|
||||||
@ -101,6 +104,19 @@ class SettingsActivity : BaseActivity() {
|
|||||||
composable("settings/appearance/colorscheme/custom") {
|
composable("settings/appearance/colorscheme/custom") {
|
||||||
CustomColorSchemeSettingsScreen()
|
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") {
|
composable("settings/appearance/cards") {
|
||||||
CardsSettingsScreen()
|
CardsSettingsScreen()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,6 +55,18 @@ fun AppearanceSettingsScreen() {
|
|||||||
navController?.navigate("settings/appearance/colorscheme")
|
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()
|
val font by viewModel.font.collectAsState()
|
||||||
ListPreference(
|
ListPreference(
|
||||||
title = stringResource(R.string.preference_font),
|
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.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.shape.CornerSize
|
import androidx.compose.foundation.shape.CornerSize
|
||||||
import androidx.compose.foundation.shape.CutCornerShape
|
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.LauncherDataStore
|
||||||
import de.mm20.launcher2.preferences.Settings
|
import de.mm20.launcher2.preferences.Settings
|
||||||
import de.mm20.launcher2.preferences.Settings.AppearanceSettings
|
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.LocalDarkTheme
|
||||||
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
|
|
||||||
import de.mm20.launcher2.ui.theme.colorscheme.*
|
import de.mm20.launcher2.ui.theme.colorscheme.*
|
||||||
import de.mm20.launcher2.ui.theme.typography.DefaultTypography
|
import de.mm20.launcher2.ui.theme.typography.DefaultTypography
|
||||||
import de.mm20.launcher2.ui.theme.typography.getDeviceDefaultTypography
|
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(
|
val themePreference by remember { dataStore.data.map { it.appearance.theme } }.collectAsState(
|
||||||
Theme.System
|
AppearanceSettings.Theme.System
|
||||||
)
|
)
|
||||||
val darkTheme =
|
val darkTheme =
|
||||||
themePreference == Theme.Dark || themePreference == Theme.System && isSystemInDarkTheme()
|
themePreference == AppearanceSettings.Theme.Dark || themePreference == AppearanceSettings.Theme.System && isSystemInDarkTheme()
|
||||||
|
|
||||||
val cornerRadius by remember {
|
val cornerRadius by remember {
|
||||||
dataStore.data.map { it.cards.radius.dp }
|
dataStore.data.map { it.cards.radius.dp }
|
||||||
@ -94,7 +95,6 @@ fun colorSchemeAsState(
|
|||||||
colorScheme: AppearanceSettings.ColorScheme,
|
colorScheme: AppearanceSettings.ColorScheme,
|
||||||
darkTheme: Boolean
|
darkTheme: Boolean
|
||||||
): MutableState<ColorScheme> {
|
): MutableState<ColorScheme> {
|
||||||
val context = LocalContext.current
|
|
||||||
val dataStore: LauncherDataStore by inject()
|
val dataStore: LauncherDataStore by inject()
|
||||||
|
|
||||||
when (colorScheme) {
|
when (colorScheme) {
|
||||||
@ -125,29 +125,14 @@ fun colorSchemeAsState(
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
if (Build.VERSION.SDK_INT >= 27 && (Build.VERSION.SDK_INT < 31 || colorScheme == AppearanceSettings.ColorScheme.DebugMaterialYouCompat)) {
|
val scheme = if (darkTheme) {
|
||||||
val wallpaperColors = LocalWallpaperColors.current
|
darkColorSchemeOf(Theme(DefaultThemeId, name = ""))
|
||||||
val state = remember(wallpaperColors, darkTheme) {
|
} else {
|
||||||
mutableStateOf(
|
lightColorSchemeOf(Theme(DefaultThemeId, name = ""))
|
||||||
MaterialYouCompatScheme(wallpaperColors, darkTheme)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return state
|
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= 31) {
|
return remember(scheme, darkTheme) {
|
||||||
return remember(darkTheme) {
|
mutableStateOf(scheme)
|
||||||
mutableStateOf(
|
|
||||||
if (darkTheme) {
|
|
||||||
dynamicDarkColorScheme(context)
|
|
||||||
} else {
|
|
||||||
dynamicLightColorScheme(context)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return remember { mutableStateOf(if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme) }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,108 @@
|
|||||||
package de.mm20.launcher2.ui.theme.colorscheme
|
package de.mm20.launcher2.ui.theme.colorscheme
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
import androidx.compose.material3.ColorScheme
|
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.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.preferences.Settings
|
||||||
|
import de.mm20.launcher2.themes.CorePalette
|
||||||
|
import de.mm20.launcher2.themes.DefaultDarkColorScheme
|
||||||
|
import de.mm20.launcher2.themes.DefaultLightColorScheme
|
||||||
|
import de.mm20.launcher2.themes.FullColorScheme
|
||||||
|
import de.mm20.launcher2.themes.PartialCorePalette
|
||||||
|
import de.mm20.launcher2.themes.Theme
|
||||||
|
import de.mm20.launcher2.themes.get
|
||||||
|
import de.mm20.launcher2.themes.merge
|
||||||
|
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
|
||||||
|
|
||||||
fun CustomColorScheme(colors: Settings.AppearanceSettings.CustomColors.Scheme) : ColorScheme {
|
@Composable
|
||||||
|
fun lightColorSchemeOf(theme: Theme): ColorScheme {
|
||||||
|
return colorSchemeOf(theme.lightColorScheme.merge(DefaultLightColorScheme), theme.corePalette)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun darkColorSchemeOf(theme: Theme): ColorScheme {
|
||||||
|
return colorSchemeOf(theme.darkColorScheme.merge(DefaultDarkColorScheme), theme.corePalette)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun colorSchemeOf(colorScheme: FullColorScheme, corePalette: PartialCorePalette): ColorScheme {
|
||||||
|
val defaultPalette = systemCorePalette()
|
||||||
|
return remember(colorScheme, corePalette, defaultPalette) {
|
||||||
|
val mergedCorePalette = corePalette.merge(defaultPalette)
|
||||||
|
ColorScheme(
|
||||||
|
primary = Color(colorScheme.primary.get(mergedCorePalette)),
|
||||||
|
onPrimary = Color(colorScheme.onPrimary.get(mergedCorePalette)),
|
||||||
|
primaryContainer = Color(colorScheme.primaryContainer.get(mergedCorePalette)),
|
||||||
|
onPrimaryContainer = Color(colorScheme.onPrimaryContainer.get(mergedCorePalette)),
|
||||||
|
secondary = Color(colorScheme.secondary.get(mergedCorePalette)),
|
||||||
|
onSecondary = Color(colorScheme.onSecondary.get(mergedCorePalette)),
|
||||||
|
secondaryContainer = Color(colorScheme.secondaryContainer.get(mergedCorePalette)),
|
||||||
|
onSecondaryContainer = Color(colorScheme.onSecondaryContainer.get(mergedCorePalette)),
|
||||||
|
tertiary = Color(colorScheme.tertiary.get(mergedCorePalette)),
|
||||||
|
onTertiary = Color(colorScheme.onTertiary.get(mergedCorePalette)),
|
||||||
|
tertiaryContainer = Color(colorScheme.tertiaryContainer.get(mergedCorePalette)),
|
||||||
|
onTertiaryContainer = Color(colorScheme.onTertiaryContainer.get(mergedCorePalette)),
|
||||||
|
error = Color(colorScheme.error.get(mergedCorePalette)),
|
||||||
|
onError = Color(colorScheme.onError.get(mergedCorePalette)),
|
||||||
|
errorContainer = Color(colorScheme.errorContainer.get(mergedCorePalette)),
|
||||||
|
onErrorContainer = Color(colorScheme.onErrorContainer.get(mergedCorePalette)),
|
||||||
|
surface = Color(colorScheme.surface.get(mergedCorePalette)),
|
||||||
|
onSurface = Color(colorScheme.onSurface.get(mergedCorePalette)),
|
||||||
|
onSurfaceVariant = Color(colorScheme.onSurfaceVariant.get(mergedCorePalette)),
|
||||||
|
outline = Color(colorScheme.outline.get(mergedCorePalette)),
|
||||||
|
outlineVariant = Color(colorScheme.outlineVariant.get(mergedCorePalette)),
|
||||||
|
surfaceContainerLowest = Color(colorScheme.surfaceContainerLowest.get(mergedCorePalette)),
|
||||||
|
surfaceContainerLow = Color(colorScheme.surfaceContainerLow.get(mergedCorePalette)),
|
||||||
|
surfaceContainer = Color(colorScheme.surfaceContainer.get(mergedCorePalette)),
|
||||||
|
surfaceContainerHigh = Color(colorScheme.surfaceContainerHigh.get(mergedCorePalette)),
|
||||||
|
surfaceContainerHighest = Color(colorScheme.surfaceContainerHighest.get(mergedCorePalette)),
|
||||||
|
surfaceDim = Color(colorScheme.surfaceDim.get(mergedCorePalette)),
|
||||||
|
surfaceBright = Color(colorScheme.surfaceBright.get(mergedCorePalette)),
|
||||||
|
inverseOnSurface = Color(colorScheme.inverseOnSurface.get(mergedCorePalette)),
|
||||||
|
inverseSurface = Color(colorScheme.inverseSurface.get(mergedCorePalette)),
|
||||||
|
inversePrimary = Color(colorScheme.inversePrimary.get(mergedCorePalette)),
|
||||||
|
surfaceTint = Color(colorScheme.surfaceTint.get(mergedCorePalette)),
|
||||||
|
background = Color(colorScheme.background.get(mergedCorePalette)),
|
||||||
|
onBackground = Color(colorScheme.onBackground.get(mergedCorePalette)),
|
||||||
|
scrim = Color(colorScheme.scrim.get(mergedCorePalette)),
|
||||||
|
surfaceVariant = Color(colorScheme.surfaceVariant.get(mergedCorePalette)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun systemCorePalette(): CorePalette<Int> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
if (Build.VERSION.SDK_INT >= 31) {
|
||||||
|
return CorePalette(
|
||||||
|
primary = ContextCompat.getColor(context, android.R.color.system_accent1_500),
|
||||||
|
secondary = ContextCompat.getColor(context, android.R.color.system_accent2_500),
|
||||||
|
tertiary = ContextCompat.getColor(context, android.R.color.system_accent3_500),
|
||||||
|
neutral = ContextCompat.getColor(context, android.R.color.system_neutral1_500),
|
||||||
|
neutralVariant = ContextCompat.getColor(context, android.R.color.system_neutral2_500),
|
||||||
|
error = 0xFFB3261E.toInt(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val wallpaperColors = LocalWallpaperColors.current
|
||||||
|
return remember(wallpaperColors) {
|
||||||
|
val corePalette = palettes.CorePalette.of(wallpaperColors.primary.toArgb())
|
||||||
|
CorePalette(
|
||||||
|
primary = corePalette.a1.tone(40),
|
||||||
|
secondary = corePalette.a2.tone(40),
|
||||||
|
tertiary = corePalette.a3.tone(40),
|
||||||
|
neutral = corePalette.n1.tone(40),
|
||||||
|
neutralVariant = corePalette.n2.tone(40),
|
||||||
|
error = corePalette.error.tone(40),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun CustomColorScheme(colors: Settings.AppearanceSettings.CustomColors.Scheme): ColorScheme {
|
||||||
return ColorScheme(
|
return ColorScheme(
|
||||||
primary = Color(colors.primary),
|
primary = Color(colors.primary),
|
||||||
onPrimary = Color(colors.onPrimary),
|
onPrimary = Color(colors.onPrimary),
|
||||||
|
|||||||
@ -436,6 +436,7 @@
|
|||||||
<string name="preference_custom_colors_n1">Neutral</string>
|
<string name="preference_custom_colors_n1">Neutral</string>
|
||||||
<string name="preference_custom_colors_n2">Neutral Variant</string>
|
<string name="preference_custom_colors_n2">Neutral Variant</string>
|
||||||
<string name="preference_custom_colors_error">Error</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_advanced_mode">Advanced mode</string>
|
||||||
<string name="preference_custom_colors_simple_mode">Simple mode</string>
|
<string name="preference_custom_colors_simple_mode">Simple mode</string>
|
||||||
<string name="preference_colors_auto_generate">Generate from primary color</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