Custom color schemes v2 - Part 1
Add data structures and theme editor
This commit is contained in:
@@ -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 = "#",
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
+5
-7
@@ -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 {
|
||||
@@ -11,4 +12,9 @@ fun Color.toHexString(): String {
|
||||
red.toString(16).run { if (length == 1) "0$this" else this } +
|
||||
green.toString(16).run { if (length == 1) "0$this" else this } +
|
||||
blue.toString(16).run { if (length == 1) "0$this" else this }
|
||||
}
|
||||
|
||||
fun Color.Companion.hct(hue: Float, chroma: Float, tone: Float): Color {
|
||||
val hct = Hct.from(hue.toDouble(), chroma.toDouble(), tone.toDouble())
|
||||
return Color(hct.toInt())
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
+12
@@ -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),
|
||||
|
||||
+1856
File diff suppressed because it is too large
Load Diff
+134
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -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,29 +125,14 @@ fun colorSchemeAsState(
|
||||
return state
|
||||
}
|
||||
else -> {
|
||||
if (Build.VERSION.SDK_INT >= 27 && (Build.VERSION.SDK_INT < 31 || colorScheme == AppearanceSettings.ColorScheme.DebugMaterialYouCompat)) {
|
||||
val wallpaperColors = LocalWallpaperColors.current
|
||||
val state = remember(wallpaperColors, darkTheme) {
|
||||
mutableStateOf(
|
||||
MaterialYouCompatScheme(wallpaperColors, darkTheme)
|
||||
)
|
||||
}
|
||||
return state
|
||||
val scheme = if (darkTheme) {
|
||||
darkColorSchemeOf(Theme(DefaultThemeId, name = ""))
|
||||
} else {
|
||||
lightColorSchemeOf(Theme(DefaultThemeId, name = ""))
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
return remember(darkTheme) {
|
||||
mutableStateOf(
|
||||
if (darkTheme) {
|
||||
dynamicDarkColorScheme(context)
|
||||
} else {
|
||||
dynamicLightColorScheme(context)
|
||||
}
|
||||
)
|
||||
}
|
||||
return remember(scheme, darkTheme) {
|
||||
mutableStateOf(scheme)
|
||||
}
|
||||
|
||||
return remember { mutableStateOf(if (darkTheme) DarkDefaultColorScheme else LightDefaultColorScheme) }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,108 @@
|
||||
package de.mm20.launcher2.ui.theme.colorscheme
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import de.mm20.launcher2.preferences.Settings
|
||||
import de.mm20.launcher2.themes.CorePalette
|
||||
import de.mm20.launcher2.themes.DefaultDarkColorScheme
|
||||
import de.mm20.launcher2.themes.DefaultLightColorScheme
|
||||
import de.mm20.launcher2.themes.FullColorScheme
|
||||
import de.mm20.launcher2.themes.PartialCorePalette
|
||||
import de.mm20.launcher2.themes.Theme
|
||||
import de.mm20.launcher2.themes.get
|
||||
import de.mm20.launcher2.themes.merge
|
||||
import de.mm20.launcher2.ui.locals.LocalWallpaperColors
|
||||
|
||||
fun CustomColorScheme(colors: Settings.AppearanceSettings.CustomColors.Scheme) : ColorScheme {
|
||||
@Composable
|
||||
fun lightColorSchemeOf(theme: Theme): ColorScheme {
|
||||
return colorSchemeOf(theme.lightColorScheme.merge(DefaultLightColorScheme), theme.corePalette)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun darkColorSchemeOf(theme: Theme): ColorScheme {
|
||||
return colorSchemeOf(theme.darkColorScheme.merge(DefaultDarkColorScheme), theme.corePalette)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun colorSchemeOf(colorScheme: FullColorScheme, corePalette: PartialCorePalette): ColorScheme {
|
||||
val defaultPalette = systemCorePalette()
|
||||
return remember(colorScheme, corePalette, defaultPalette) {
|
||||
val mergedCorePalette = corePalette.merge(defaultPalette)
|
||||
ColorScheme(
|
||||
primary = Color(colorScheme.primary.get(mergedCorePalette)),
|
||||
onPrimary = Color(colorScheme.onPrimary.get(mergedCorePalette)),
|
||||
primaryContainer = Color(colorScheme.primaryContainer.get(mergedCorePalette)),
|
||||
onPrimaryContainer = Color(colorScheme.onPrimaryContainer.get(mergedCorePalette)),
|
||||
secondary = Color(colorScheme.secondary.get(mergedCorePalette)),
|
||||
onSecondary = Color(colorScheme.onSecondary.get(mergedCorePalette)),
|
||||
secondaryContainer = Color(colorScheme.secondaryContainer.get(mergedCorePalette)),
|
||||
onSecondaryContainer = Color(colorScheme.onSecondaryContainer.get(mergedCorePalette)),
|
||||
tertiary = Color(colorScheme.tertiary.get(mergedCorePalette)),
|
||||
onTertiary = Color(colorScheme.onTertiary.get(mergedCorePalette)),
|
||||
tertiaryContainer = Color(colorScheme.tertiaryContainer.get(mergedCorePalette)),
|
||||
onTertiaryContainer = Color(colorScheme.onTertiaryContainer.get(mergedCorePalette)),
|
||||
error = Color(colorScheme.error.get(mergedCorePalette)),
|
||||
onError = Color(colorScheme.onError.get(mergedCorePalette)),
|
||||
errorContainer = Color(colorScheme.errorContainer.get(mergedCorePalette)),
|
||||
onErrorContainer = Color(colorScheme.onErrorContainer.get(mergedCorePalette)),
|
||||
surface = Color(colorScheme.surface.get(mergedCorePalette)),
|
||||
onSurface = Color(colorScheme.onSurface.get(mergedCorePalette)),
|
||||
onSurfaceVariant = Color(colorScheme.onSurfaceVariant.get(mergedCorePalette)),
|
||||
outline = Color(colorScheme.outline.get(mergedCorePalette)),
|
||||
outlineVariant = Color(colorScheme.outlineVariant.get(mergedCorePalette)),
|
||||
surfaceContainerLowest = Color(colorScheme.surfaceContainerLowest.get(mergedCorePalette)),
|
||||
surfaceContainerLow = Color(colorScheme.surfaceContainerLow.get(mergedCorePalette)),
|
||||
surfaceContainer = Color(colorScheme.surfaceContainer.get(mergedCorePalette)),
|
||||
surfaceContainerHigh = Color(colorScheme.surfaceContainerHigh.get(mergedCorePalette)),
|
||||
surfaceContainerHighest = Color(colorScheme.surfaceContainerHighest.get(mergedCorePalette)),
|
||||
surfaceDim = Color(colorScheme.surfaceDim.get(mergedCorePalette)),
|
||||
surfaceBright = Color(colorScheme.surfaceBright.get(mergedCorePalette)),
|
||||
inverseOnSurface = Color(colorScheme.inverseOnSurface.get(mergedCorePalette)),
|
||||
inverseSurface = Color(colorScheme.inverseSurface.get(mergedCorePalette)),
|
||||
inversePrimary = Color(colorScheme.inversePrimary.get(mergedCorePalette)),
|
||||
surfaceTint = Color(colorScheme.surfaceTint.get(mergedCorePalette)),
|
||||
background = Color(colorScheme.background.get(mergedCorePalette)),
|
||||
onBackground = Color(colorScheme.onBackground.get(mergedCorePalette)),
|
||||
scrim = Color(colorScheme.scrim.get(mergedCorePalette)),
|
||||
surfaceVariant = Color(colorScheme.surfaceVariant.get(mergedCorePalette)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun systemCorePalette(): CorePalette<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(
|
||||
primary = Color(colors.primary),
|
||||
onPrimary = Color(colors.onPrimary),
|
||||
|
||||
Reference in New Issue
Block a user