mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2026-01-20 18:52:33 +01:00
Rip the theming from Tachiyomi 1.x and piece it together for JUI
This commit is contained in:
@@ -14,7 +14,7 @@ import ca.gosyer.data.ui.model.ThemeMode
|
||||
class UiPreferences(private val preferenceStore: PreferenceStore) {
|
||||
|
||||
fun themeMode(): Preference<ThemeMode> {
|
||||
return preferenceStore.getJsonObject("theme_mode", ThemeMode.System, ThemeMode.serializer())
|
||||
return preferenceStore.getJsonObject("theme_mode", ThemeMode.Light, ThemeMode.serializer())
|
||||
}
|
||||
|
||||
fun lightTheme(): Preference<Int> {
|
||||
@@ -41,14 +41,6 @@ class UiPreferences(private val preferenceStore: PreferenceStore) {
|
||||
return preferenceStore.getInt("color_secondary_dark", 0)
|
||||
}
|
||||
|
||||
fun colorBarsLight(): Preference<Int> {
|
||||
return preferenceStore.getInt("color_bar_light", 0)
|
||||
}
|
||||
|
||||
fun colorBarsDark(): Preference<Int> {
|
||||
return preferenceStore.getInt("color_bar_dark", 0)
|
||||
}
|
||||
|
||||
fun startScreen(): Preference<Screen> {
|
||||
return preferenceStore.getJsonObject("start_screen", Screen.Library, Screen.serializer())
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
enum class ThemeMode {
|
||||
System,
|
||||
/*System,*/
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
@@ -89,3 +89,44 @@ fun WindowDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun WindowDialog(
|
||||
title: String = "Dialog",
|
||||
size: IntSize = IntSize(400, 200),
|
||||
onDismissRequest: (() -> Unit)? = null,
|
||||
forceFocus: Boolean = true,
|
||||
buttons: @Composable (AppWindow) -> Unit,
|
||||
content: @Composable (AppWindow) -> Unit
|
||||
) = SwingUtilities.invokeLater {
|
||||
val window = AppWindow(
|
||||
title = title,
|
||||
size = size,
|
||||
location = IntOffset.Zero,
|
||||
centered = true,
|
||||
icon = null,
|
||||
menuBar = null,
|
||||
undecorated = false,
|
||||
events = WindowEvents(),
|
||||
onDismissRequest = onDismissRequest
|
||||
)
|
||||
|
||||
if (forceFocus) {
|
||||
window.events.onFocusLost = {
|
||||
window.window.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
window.show {
|
||||
MaterialTheme {
|
||||
Surface {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
content(window)
|
||||
buttons(window)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,454 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.base.components
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.forEachGesture
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
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.layout.requiredHeight
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.GridCells
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.isSpecified
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.pointer.PointerInputScope
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.ui.base.WindowDialog
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlin.math.round
|
||||
|
||||
fun ColorPickerDialog(
|
||||
title: String,
|
||||
onDismissRequest: () -> Unit = {},
|
||||
onSelected: (Color) -> Unit,
|
||||
initialColor: Color = Color.Unspecified,
|
||||
) {
|
||||
val currentColor = MutableStateFlow(initialColor)
|
||||
val showPresets = MutableStateFlow(true)
|
||||
|
||||
WindowDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = title,
|
||||
content = {
|
||||
val showPresetsState by showPresets.collectAsState()
|
||||
val currentColorState by currentColor.collectAsState()
|
||||
if (showPresetsState) {
|
||||
ColorPresets(
|
||||
initialColor = currentColorState,
|
||||
onColorChanged = { currentColor.value = it }
|
||||
)
|
||||
} else {
|
||||
ColorPalette(
|
||||
initialColor = currentColorState,
|
||||
onColorChanged = { currentColor.value = it }
|
||||
)
|
||||
}
|
||||
},
|
||||
buttons = {
|
||||
val showPresetsState by showPresets.collectAsState()
|
||||
val currentColorState by currentColor.collectAsState()
|
||||
Row(Modifier.fillMaxWidth().padding(8.dp)) {
|
||||
TextButton(onClick = {
|
||||
showPresets.value = !showPresetsState
|
||||
}) {
|
||||
Text(if (showPresetsState) "Custom" else "Presets")
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
TextButton(onClick = {
|
||||
onSelected(currentColorState)
|
||||
it.close()
|
||||
}) {
|
||||
Text("Select")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun ColorPresets(
|
||||
initialColor: Color,
|
||||
onColorChanged: (Color) -> Unit
|
||||
) {
|
||||
val presets = remember {
|
||||
if (initialColor.isSpecified) {
|
||||
(listOf(initialColor) + presetColors).distinct()
|
||||
} else {
|
||||
presetColors
|
||||
}
|
||||
}
|
||||
|
||||
var selectedColor by remember { mutableStateOf(initialColor.takeOrElse { presets.first() }) }
|
||||
var selectedShade by remember { mutableStateOf<Color?>(null) }
|
||||
|
||||
val shades = remember(selectedColor) { getColorShades(selectedColor) }
|
||||
|
||||
val borderColor = MaterialTheme.colors.onBackground.copy(alpha = 0.54f)
|
||||
|
||||
Column {
|
||||
LazyVerticalGrid(cells = GridCells.Fixed(5)) {
|
||||
items(presets) { color ->
|
||||
ColorPresetItem(
|
||||
color = color,
|
||||
borderColor = borderColor,
|
||||
isSelected = selectedShade == null && initialColor == color,
|
||||
onClick = {
|
||||
selectedShade = null
|
||||
selectedColor = color
|
||||
onColorChanged(color)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(
|
||||
modifier = Modifier.padding(vertical = 16.dp).fillMaxWidth().requiredHeight(1.dp)
|
||||
.background(MaterialTheme.colors.onBackground.copy(alpha = 0.2f))
|
||||
)
|
||||
|
||||
LazyRow {
|
||||
items(shades) { color ->
|
||||
ColorPresetItem(
|
||||
color = color,
|
||||
borderColor = borderColor,
|
||||
isSelected = selectedShade == color,
|
||||
onClick = {
|
||||
selectedShade = color
|
||||
onColorChanged(color)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColorPresetItem(
|
||||
color: Color,
|
||||
borderColor: Color,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center, modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(4.dp)
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
.border(BorderStroke(1.dp, borderColor), CircleShape)
|
||||
.clickable(onClick = onClick)
|
||||
) {
|
||||
if (isSelected) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
tint = if (color.luminance() > 0.5) Color.Black else Color.White,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.requiredWidth(32.dp).requiredHeight(32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getColorShades(color: Color): List<Color> {
|
||||
val f = String.format("%06X", 0xFFFFFF and color.toArgb()).toLong(16)
|
||||
return listOf(
|
||||
shadeColor(f, 0.9), shadeColor(f, 0.7), shadeColor(f, 0.5),
|
||||
shadeColor(f, 0.333), shadeColor(f, 0.166), shadeColor(f, -0.125),
|
||||
shadeColor(f, -0.25), shadeColor(f, -0.375), shadeColor(f, -0.5),
|
||||
shadeColor(f, -0.675), shadeColor(f, -0.7), shadeColor(f, -0.775)
|
||||
)
|
||||
}
|
||||
|
||||
private fun shadeColor(f: Long, percent: Double): Color {
|
||||
val t = if (percent < 0) 0.0 else 255.0
|
||||
val p = if (percent < 0) percent * -1 else percent
|
||||
val r = f shr 16
|
||||
val g = f shr 8 and 0x00FF
|
||||
val b = f and 0x0000FF
|
||||
|
||||
val red = (round((t - r) * p) + r).toInt()
|
||||
val green = (round((t - g) * p) + g).toInt()
|
||||
val blue = (round((t - b) * p) + b).toInt()
|
||||
return Color(red = red, green = green, blue = blue, alpha = 255)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColorPalette(
|
||||
initialColor: Color = Color.White,
|
||||
onColorChanged: (Color) -> Unit = {}
|
||||
) {
|
||||
var selectedColor by remember { mutableStateOf(initialColor) }
|
||||
var textFieldHex by remember { mutableStateOf(initialColor.toHexString()) }
|
||||
|
||||
var hue by remember { mutableStateOf(initialColor.toHsv()[0]) }
|
||||
var hueCursor by remember { mutableStateOf(0f) }
|
||||
|
||||
var matrixSize by remember { mutableStateOf(IntSize(0, 0)) }
|
||||
var matrixCursor by remember { mutableStateOf(Offset(0f, 0f)) }
|
||||
|
||||
val saturationGradient = remember(hue, matrixSize) {
|
||||
Brush.linearGradient(
|
||||
colors = listOf(Color.White, hueToColor(hue)),
|
||||
start = Offset(0f, 0f), end = Offset(matrixSize.width.toFloat(), 0f)
|
||||
)
|
||||
}
|
||||
val valueGradient = remember(matrixSize) {
|
||||
Brush.linearGradient(
|
||||
colors = listOf(Color.White, Color.Black),
|
||||
start = Offset(0f, 0f), end = Offset(0f, matrixSize.height.toFloat())
|
||||
)
|
||||
}
|
||||
|
||||
val cursorColor = MaterialTheme.colors.onBackground
|
||||
val cursorStroke = Stroke(4f)
|
||||
val borderStroke = Stroke(1f)
|
||||
|
||||
fun setSelectedColor(color: Color, invalidate: Boolean = false) {
|
||||
selectedColor = color
|
||||
textFieldHex = color.toHexString()
|
||||
if (invalidate) {
|
||||
val hsv = color.toHsv()
|
||||
hue = hsv[0]
|
||||
matrixCursor = satValToCoordinates(hsv[1], hsv[2], matrixSize)
|
||||
hueCursor = hueToCoordinate(hsv[0], matrixSize)
|
||||
}
|
||||
onColorChanged(color)
|
||||
}
|
||||
|
||||
Column {
|
||||
Text("") // TODO workaround: without this text, the color picker doesn't render correctly
|
||||
Row(Modifier.height(IntrinsicSize.Max)) {
|
||||
Box(Modifier
|
||||
.aspectRatio(1f)
|
||||
.weight(1f)
|
||||
.onSizeChanged {
|
||||
matrixSize = it
|
||||
val hsv = selectedColor.toHsv()
|
||||
matrixCursor = satValToCoordinates(hsv[1], hsv[2], it)
|
||||
hueCursor = hueToCoordinate(hue, it)
|
||||
}
|
||||
.drawWithContent {
|
||||
drawRect(brush = valueGradient)
|
||||
drawRect(brush = saturationGradient, blendMode = BlendMode.Multiply)
|
||||
drawRect(Color.LightGray, size = size, style = borderStroke)
|
||||
drawCircle(
|
||||
Color.Black,
|
||||
radius = 8f,
|
||||
center = matrixCursor,
|
||||
style = cursorStroke
|
||||
)
|
||||
drawCircle(
|
||||
Color.LightGray,
|
||||
radius = 12f,
|
||||
center = matrixCursor,
|
||||
style = cursorStroke
|
||||
)
|
||||
}
|
||||
.pointerInput(Unit) {
|
||||
detectMove { offset ->
|
||||
val safeOffset = offset.copy(
|
||||
x = offset.x.coerceIn(0f, matrixSize.width.toFloat()),
|
||||
y = offset.y.coerceIn(0f, matrixSize.height.toFloat())
|
||||
)
|
||||
matrixCursor = safeOffset
|
||||
val newColor = matrixCoordinatesToColor(hue, safeOffset, matrixSize)
|
||||
setSelectedColor(newColor)
|
||||
}
|
||||
}
|
||||
)
|
||||
Box(Modifier
|
||||
.fillMaxHeight()
|
||||
.requiredWidth(48.dp)
|
||||
.padding(start = 8.dp)
|
||||
.drawWithCache {
|
||||
var h = 360f
|
||||
val colors = MutableList(size.height.toInt()) {
|
||||
hueToColor(h).also {
|
||||
h -= 360f / size.height
|
||||
}
|
||||
}
|
||||
val cursorSize = Size(size.width, 10f)
|
||||
val cursorTopLeft = Offset(0f, hueCursor - (cursorSize.height / 2))
|
||||
onDrawBehind {
|
||||
colors.forEachIndexed { i, color ->
|
||||
val pos = i.toFloat()
|
||||
drawLine(color, Offset(0f, pos), Offset(size.width, pos))
|
||||
}
|
||||
drawRect(Color.LightGray, size = size, style = borderStroke)
|
||||
drawRect(
|
||||
cursorColor,
|
||||
topLeft = cursorTopLeft,
|
||||
size = cursorSize,
|
||||
style = cursorStroke
|
||||
)
|
||||
}
|
||||
}
|
||||
.pointerInput(Unit) {
|
||||
detectMove { offset ->
|
||||
val safeY = offset.y.coerceIn(0f, matrixSize.height.toFloat())
|
||||
hueCursor = safeY
|
||||
hue = hueCoordinatesToHue(safeY, matrixSize)
|
||||
val newColor = matrixCoordinatesToColor(hue, matrixCursor, matrixSize)
|
||||
setSelectedColor(newColor)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
Row(Modifier.padding(top = 8.dp), verticalAlignment = Alignment.Bottom) {
|
||||
Box(
|
||||
Modifier.size(72.dp, 48.dp).background(selectedColor)
|
||||
.border(1.dp, MaterialTheme.colors.onBackground.copy(alpha = 0.54f))
|
||||
)
|
||||
Spacer(Modifier.requiredWidth(32.dp))
|
||||
OutlinedTextField(
|
||||
value = textFieldHex,
|
||||
onValueChange = {
|
||||
val newColor = hexStringToColor(it)
|
||||
if (newColor != null) {
|
||||
setSelectedColor(newColor, invalidate = true)
|
||||
} else {
|
||||
textFieldHex = it
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun PointerInputScope.detectMove(onMove: (Offset) -> Unit) {
|
||||
forEachGesture {
|
||||
awaitPointerEventScope {
|
||||
var change = awaitFirstDown()
|
||||
while (change.pressed) {
|
||||
onMove(change.position)
|
||||
change = awaitPointerEvent().changes.first()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Coordinates <-> Color
|
||||
|
||||
private fun matrixCoordinatesToColor(hue: Float, position: Offset, size: IntSize): Color {
|
||||
val saturation = 1f / size.width * position.x
|
||||
val value = 1f - (1f / size.height * position.y)
|
||||
return hsvToColor(hue, saturation, value)
|
||||
}
|
||||
|
||||
private fun hueCoordinatesToHue(y: Float, size: IntSize): Float {
|
||||
val hue = 360f - y * 360f / size.height
|
||||
return hsvToColor(hue, 1f, 1f).toHsv()[0]
|
||||
}
|
||||
|
||||
private fun satValToCoordinates(saturation: Float, value: Float, size: IntSize): Offset {
|
||||
return Offset(saturation * size.width, ((1f - value) * size.height))
|
||||
}
|
||||
|
||||
private fun hueToCoordinate(hue: Float, size: IntSize): Float {
|
||||
return size.height - (hue * size.height / 360f)
|
||||
}
|
||||
|
||||
// Color space conversions
|
||||
|
||||
fun hsvToColor(hue: Float, saturation: Float, value: Float): Color {
|
||||
return Color(java.awt.Color.HSBtoRGB(hue, saturation, value))
|
||||
}
|
||||
|
||||
private fun Color.toHsv(): FloatArray {
|
||||
fun Float.toIntColor() = (this * 256).toInt()
|
||||
val result = floatArrayOf(0f, 0f, 0f)
|
||||
java.awt.Color.RGBtoHSB(red.toIntColor(), green.toIntColor(), blue.toIntColor(), result)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun hueToColor(hue: Float): Color {
|
||||
return hsvToColor(hue, 1f, 1f)
|
||||
}
|
||||
|
||||
private fun Color.toHexString(): String {
|
||||
return String.format("#%06X", (0xFFFFFF and toArgb()))
|
||||
}
|
||||
|
||||
private fun hexStringToColor(hex: String): Color? {
|
||||
return try {
|
||||
val color = java.awt.Color.decode(hex)
|
||||
Color(color.red, color.green, color.blue, color.alpha)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val presetColors = listOf(
|
||||
Color(0xFFF44336), // RED 500
|
||||
Color(0xFFE91E63), // PINK 500
|
||||
Color(0xFFFF2C93), // LIGHT PINK 500
|
||||
Color(0xFF9C27B0), // PURPLE 500
|
||||
Color(0xFF673AB7), // DEEP PURPLE 500
|
||||
Color(0xFF3F51B5), // INDIGO 500
|
||||
Color(0xFF2196F3), // BLUE 500
|
||||
Color(0xFF03A9F4), // LIGHT BLUE 500
|
||||
Color(0xFF00BCD4), // CYAN 500
|
||||
Color(0xFF009688), // TEAL 500
|
||||
Color(0xFF4CAF50), // GREEN 500
|
||||
Color(0xFF8BC34A), // LIGHT GREEN 500
|
||||
Color(0xFFCDDC39), // LIME 500
|
||||
Color(0xFFFFEB3B), // YELLOW 500
|
||||
Color(0xFFFFC107), // AMBER 500
|
||||
Color(0xFFFF9800), // ORANGE 500
|
||||
Color(0xFF795548), // BROWN 500
|
||||
Color(0xFF607D8B), // BLUE GREY 500
|
||||
Color(0xFF9E9E9E), // GREY 500
|
||||
)
|
||||
|
||||
29
src/main/kotlin/ca/gosyer/ui/base/components/Scrollable.kt
Normal file
29
src/main/kotlin/ca/gosyer/ui/base/components/Scrollable.kt
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.base.components
|
||||
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun ScrollableColumn(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.scrollable(
|
||||
state = rememberScrollState(),
|
||||
orientation = Orientation.Vertical
|
||||
)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
67
src/main/kotlin/ca/gosyer/ui/base/prefs/ColorPreference.kt
Normal file
67
src/main/kotlin/ca/gosyer/ui/base/prefs/ColorPreference.kt
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.base.prefs
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import ca.gosyer.common.prefs.Preference
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
|
||||
class ColorPreference(
|
||||
private val preference: Preference<Int>
|
||||
) : Preference<Color> {
|
||||
|
||||
override fun key(): String {
|
||||
return preference.key()
|
||||
}
|
||||
|
||||
override fun get(): Color {
|
||||
return if (isSet()) {
|
||||
Color(preference.get())
|
||||
} else {
|
||||
Color.Unspecified
|
||||
}
|
||||
}
|
||||
|
||||
override fun set(value: Color) {
|
||||
if (value != Color.Unspecified) {
|
||||
preference.set(value.toArgb())
|
||||
} else {
|
||||
preference.delete()
|
||||
}
|
||||
}
|
||||
|
||||
override fun isSet(): Boolean {
|
||||
return preference.isSet()
|
||||
}
|
||||
|
||||
override fun delete() {
|
||||
preference.delete()
|
||||
}
|
||||
|
||||
override fun defaultValue(): Color {
|
||||
return Color.Unspecified
|
||||
}
|
||||
|
||||
override fun changes(): Flow<Color> {
|
||||
return preference.changes()
|
||||
.map { get() }
|
||||
}
|
||||
|
||||
override fun stateIn(scope: CoroutineScope): StateFlow<Color> {
|
||||
return preference.changes().map { get() }.stateIn(scope, SharingStarted.Eagerly, get())
|
||||
}
|
||||
}
|
||||
|
||||
fun Preference<Int>.asColor(): ColorPreference {
|
||||
return ColorPreference(this)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.base.prefs
|
||||
|
||||
import ca.gosyer.common.prefs.Preference
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class PreferenceMutableStateFlow<T>(
|
||||
private val preference: Preference<T>,
|
||||
scope: CoroutineScope,
|
||||
private val state: MutableStateFlow<T> = MutableStateFlow(preference.get())
|
||||
) : MutableStateFlow<T> by state {
|
||||
|
||||
init {
|
||||
preference.changes()
|
||||
.onEach { value = it }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
override var value: T
|
||||
get() = state.value
|
||||
set(value) {
|
||||
preference.set(value)
|
||||
}
|
||||
}
|
||||
|
||||
fun <T> Preference<T>.asStateIn(scope: CoroutineScope): PreferenceMutableStateFlow<T> {
|
||||
return PreferenceMutableStateFlow(this, scope)
|
||||
}
|
||||
221
src/main/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt
Normal file
221
src/main/kotlin/ca/gosyer/ui/base/prefs/PreferencesUiBuilder.kt
Normal file
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.base.prefs
|
||||
|
||||
import androidx.compose.desktop.AppWindow
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.RadioButton
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.ui.base.WindowDialog
|
||||
import ca.gosyer.ui.base.components.ColorPickerDialog
|
||||
import ca.gosyer.ui.base.components.ScrollableColumn
|
||||
|
||||
@Composable
|
||||
fun PreferencesScrollableColumn(
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable PreferenceScope.() -> Unit
|
||||
) {
|
||||
Box {
|
||||
ScrollableColumn(modifier) {
|
||||
val scope = PreferenceScope()
|
||||
scope.content()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PreferenceScope {
|
||||
@Composable
|
||||
fun <Key> ChoicePref(
|
||||
preference: PreferenceMutableStateFlow<Key>,
|
||||
choices: Map<Key, String>,
|
||||
title: String,
|
||||
subtitle: String? = null
|
||||
) {
|
||||
Pref(
|
||||
title = title,
|
||||
subtitle = if (subtitle == null) choices[preference.value] else null,
|
||||
onClick = {
|
||||
ChoiceDialog(
|
||||
items = choices.toList(),
|
||||
selected = preference.value,
|
||||
title = title,
|
||||
onSelected = { selected ->
|
||||
preference.value = selected
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ColorPref(
|
||||
preference: PreferenceMutableStateFlow<Color>,
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
unsetColor: Color = Color.Unspecified
|
||||
) {
|
||||
val initialColor = preference.value.takeOrElse { unsetColor }
|
||||
Pref(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
onClick = {
|
||||
ColorPickerDialog(
|
||||
title = title,
|
||||
onSelected = {
|
||||
preference.value = it
|
||||
},
|
||||
initialColor = initialColor
|
||||
)
|
||||
},
|
||||
onLongClick = { preference.value = Color.Unspecified },
|
||||
action = {
|
||||
if (preference.value != Color.Unspecified || unsetColor != Color.Unspecified) {
|
||||
val borderColor = MaterialTheme.colors.onBackground.copy(alpha = 0.54f)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.size(32.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color = initialColor)
|
||||
.border(BorderStroke(1.dp, borderColor), CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun <T> ChoiceDialog(
|
||||
items: List<Pair<T, String>>,
|
||||
selected: T?,
|
||||
onDismissRequest: () -> Unit = {},
|
||||
onSelected: (T) -> Unit,
|
||||
title: String,
|
||||
buttons: @Composable (AppWindow) -> Unit = { }
|
||||
) {
|
||||
WindowDialog(onDismissRequest = onDismissRequest, buttons = buttons, title = title, content = {
|
||||
LazyColumn {
|
||||
items(items) { (value, text) ->
|
||||
Row(
|
||||
modifier = Modifier.requiredHeight(48.dp).fillMaxWidth().clickable(
|
||||
onClick = {
|
||||
onSelected(value)
|
||||
it.close()
|
||||
}),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
RadioButton(
|
||||
selected = value == selected,
|
||||
onClick = {
|
||||
onSelected(value)
|
||||
it.close()
|
||||
},
|
||||
)
|
||||
Text(text = text, modifier = Modifier.padding(start = 24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Pref(
|
||||
title: String,
|
||||
icon: ImageVector? = null,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: () -> Unit = {},
|
||||
subtitle: String? = null,
|
||||
action: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
val height = if (subtitle != null) 72.dp else 56.dp
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().requiredHeight(height)
|
||||
.combinedClickable(
|
||||
onLongClick = onLongClick,
|
||||
onClick = onClick
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
modifier = Modifier.padding(horizontal = 16.dp).size(24.dp),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
Column(Modifier.padding(horizontal = 16.dp).weight(1f)) {
|
||||
Text(
|
||||
text = title,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
)
|
||||
if (subtitle != null) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
color = LocalContentColor.current.copy(alpha = ContentAlpha.medium),
|
||||
style = MaterialTheme.typography.subtitle1
|
||||
)
|
||||
}
|
||||
}
|
||||
if (action != null) {
|
||||
Box(Modifier.widthIn(min = 56.dp)) {
|
||||
action()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SwitchPref(
|
||||
preference: PreferenceMutableStateFlow<Boolean>,
|
||||
title: String,
|
||||
subtitle: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
) {
|
||||
Pref(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
icon = icon,
|
||||
action = { Switch(checked = preference.value, onCheckedChange = null) },
|
||||
onClick = { preference.value = !preference.value }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.base.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import ca.gosyer.common.prefs.Preference
|
||||
import ca.gosyer.data.ui.UiPreferences
|
||||
import ca.gosyer.ui.base.prefs.asColor
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
data class AppColorsPreference(
|
||||
val primary: Preference<Color>,
|
||||
val secondary: Preference<Color>
|
||||
)
|
||||
|
||||
class AppColorsPreferenceState(
|
||||
val primaryStateFlow: StateFlow<Color>,
|
||||
val secondaryStateFlow: StateFlow<Color>
|
||||
)
|
||||
|
||||
fun UiPreferences.getLightColors(): AppColorsPreference {
|
||||
return AppColorsPreference(
|
||||
colorPrimaryLight().asColor(),
|
||||
colorSecondaryLight().asColor()
|
||||
)
|
||||
}
|
||||
|
||||
fun UiPreferences.getDarkColors(): AppColorsPreference {
|
||||
return AppColorsPreference(
|
||||
colorPrimaryDark().asColor(),
|
||||
colorSecondaryDark().asColor()
|
||||
)
|
||||
}
|
||||
|
||||
fun AppColorsPreference.asState(scope: CoroutineScope): AppColorsPreferenceState {
|
||||
return AppColorsPreferenceState(
|
||||
primary.stateIn(scope),
|
||||
secondary.stateIn(scope)
|
||||
)
|
||||
}
|
||||
117
src/main/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt
Normal file
117
src/main/kotlin/ca/gosyer/ui/base/theme/AppTheme.kt
Normal file
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.base.theme
|
||||
|
||||
import androidx.compose.desktop.DesktopMaterialTheme
|
||||
import androidx.compose.material.Colors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import ca.gosyer.data.ui.UiPreferences
|
||||
import ca.gosyer.data.ui.model.ThemeMode
|
||||
import ca.gosyer.ui.base.vm.ViewModel
|
||||
import ca.gosyer.ui.base.vm.viewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Composable used to apply the application colors to [content].
|
||||
* It applies the [DesktopMaterialTheme] colors.
|
||||
*/
|
||||
@Composable
|
||||
fun AppTheme(content: @Composable () -> Unit) {
|
||||
val vm = viewModel<AppThemeViewModel>()
|
||||
val colors = vm.getColors()
|
||||
/*val systemUiController = rememberSystemUiController()*/
|
||||
|
||||
DesktopMaterialTheme(colors = colors, content = content)
|
||||
}
|
||||
|
||||
private class AppThemeViewModel @Inject constructor(
|
||||
private val uiPreferences: UiPreferences
|
||||
) : ViewModel() {
|
||||
private val themeMode = uiPreferences.themeMode().asStateFlow()
|
||||
private val lightTheme = uiPreferences.lightTheme().asStateFlow()
|
||||
private val darkTheme = uiPreferences.darkTheme().asStateFlow()
|
||||
|
||||
private val baseThemeJob = SupervisorJob()
|
||||
private val baseThemeScope = CoroutineScope(baseThemeJob)
|
||||
|
||||
@Composable
|
||||
fun getColors(): Colors {
|
||||
val themeMode by themeMode.collectAsState()
|
||||
val lightTheme by lightTheme.collectAsState()
|
||||
val darkTheme by darkTheme.collectAsState()
|
||||
|
||||
val baseTheme = getBaseTheme(themeMode, lightTheme, darkTheme)
|
||||
val colors = remember(baseTheme.colors.isLight) {
|
||||
baseThemeJob.cancelChildren()
|
||||
|
||||
if (baseTheme.colors.isLight) {
|
||||
uiPreferences.getLightColors().asState(baseThemeScope)
|
||||
} else {
|
||||
uiPreferences.getDarkColors().asState(baseThemeScope)
|
||||
}
|
||||
}
|
||||
|
||||
val primary by colors.primaryStateFlow.collectAsState()
|
||||
val secondary by colors.secondaryStateFlow.collectAsState()
|
||||
|
||||
val material = getMaterialColors(baseTheme.colors, primary, secondary)
|
||||
return material
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getBaseTheme(
|
||||
themeMode: ThemeMode,
|
||||
lightTheme: Int,
|
||||
darkTheme: Int
|
||||
): Theme {
|
||||
fun getTheme(id: Int, fallbackIsLight: Boolean): Theme {
|
||||
return themes.find { it.id == id }
|
||||
?: themes.first { it.colors.isLight == fallbackIsLight }
|
||||
}
|
||||
|
||||
return when (themeMode) {
|
||||
/*ThemeMode.System -> if (!isSystemInDarkTheme()) {
|
||||
getTheme(lightTheme, true)
|
||||
} else {
|
||||
getTheme(darkTheme, false)
|
||||
}*/
|
||||
ThemeMode.Light -> getTheme(lightTheme, true)
|
||||
ThemeMode.Dark -> getTheme(darkTheme, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMaterialColors(
|
||||
baseColors: Colors,
|
||||
colorPrimary: Color,
|
||||
colorSecondary: Color
|
||||
): Colors {
|
||||
val primary = colorPrimary.takeOrElse { baseColors.primary }
|
||||
val secondary = colorSecondary.takeOrElse { baseColors.secondary }
|
||||
return baseColors.copy(
|
||||
primary = primary,
|
||||
primaryVariant = primary,
|
||||
secondary = secondary,
|
||||
secondaryVariant = secondary,
|
||||
onPrimary = if (primary.luminance() > 0.5) Color.Black else Color.White,
|
||||
onSecondary = if (secondary.luminance() > 0.5) Color.Black else Color.White,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
baseThemeScope.cancel()
|
||||
}
|
||||
}
|
||||
41
src/main/kotlin/ca/gosyer/ui/base/theme/RandomColors.kt
Normal file
41
src/main/kotlin/ca/gosyer/ui/base/theme/RandomColors.kt
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.base.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import kotlin.math.abs
|
||||
import kotlin.random.Random
|
||||
|
||||
object RandomColors {
|
||||
private val colors = arrayOf(
|
||||
Color(0xffe57373),
|
||||
Color(0xfff06292),
|
||||
Color(0xffba68c8),
|
||||
Color(0xff9575cd),
|
||||
Color(0xff7986cb),
|
||||
Color(0xff64b5f6),
|
||||
Color(0xff4fc3f7),
|
||||
Color(0xff4dd0e1),
|
||||
Color(0xff4db6ac),
|
||||
Color(0xff81c784),
|
||||
Color(0xffaed581),
|
||||
Color(0xffff8a65),
|
||||
Color(0xffd4e157),
|
||||
Color(0xffffd54f),
|
||||
Color(0xffffb74d),
|
||||
Color(0xffa1887f),
|
||||
Color(0xff90a4ae)
|
||||
)
|
||||
|
||||
fun get(key: Any): Color {
|
||||
return colors[abs(key.hashCode()) % colors.size]
|
||||
}
|
||||
|
||||
fun random(): Color {
|
||||
return colors[Random.nextInt(colors.size)]
|
||||
}
|
||||
}
|
||||
47
src/main/kotlin/ca/gosyer/ui/base/theme/Themes.kt
Normal file
47
src/main/kotlin/ca/gosyer/ui/base/theme/Themes.kt
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.base.theme
|
||||
|
||||
import androidx.compose.material.Colors
|
||||
import androidx.compose.material.darkColors
|
||||
import androidx.compose.material.lightColors
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
data class Theme(
|
||||
val id: Int,
|
||||
val colors: Colors
|
||||
)
|
||||
|
||||
val themes = listOf(
|
||||
// Pure white
|
||||
Theme(
|
||||
1, lightColors()
|
||||
),
|
||||
// Tachiyomi 0.x default colors
|
||||
Theme(
|
||||
2, lightColors(
|
||||
primary = Color(0xFF2979FF),
|
||||
primaryVariant = Color(0xFF2979FF),
|
||||
onPrimary = Color.White,
|
||||
secondary = Color(0xFF2979FF),
|
||||
secondaryVariant = Color(0xFF2979FF),
|
||||
onSecondary = Color.White
|
||||
)
|
||||
),
|
||||
// Tachiyomi 0.x dark theme
|
||||
Theme(
|
||||
3, darkColors()
|
||||
),
|
||||
// AMOLED theme
|
||||
Theme(
|
||||
4, darkColors(
|
||||
primary = Color.Black,
|
||||
onPrimary = Color.White,
|
||||
background = Color.Black
|
||||
)
|
||||
),
|
||||
)
|
||||
@@ -6,8 +6,15 @@
|
||||
|
||||
package ca.gosyer.ui.base.vm
|
||||
|
||||
import ca.gosyer.common.prefs.Preference
|
||||
import ca.gosyer.ui.base.prefs.PreferenceMutableStateFlow
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
abstract class ViewModel {
|
||||
|
||||
@@ -19,4 +26,14 @@ abstract class ViewModel {
|
||||
}
|
||||
|
||||
open fun onDestroy() {}
|
||||
|
||||
fun <T> Preference<T>.asStateFlow() = PreferenceMutableStateFlow(this, scope)
|
||||
|
||||
fun <T> Flow<T>.asStateFlow(initialValue: T): StateFlow<T> {
|
||||
val state = MutableStateFlow(initialValue)
|
||||
scope.launch {
|
||||
collect { state.value = it }
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ package ca.gosyer.ui.main
|
||||
|
||||
import androidx.compose.desktop.AppWindow
|
||||
import androidx.compose.desktop.DesktopMaterialTheme
|
||||
import androidx.compose.desktop.WindowEvents
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -16,7 +15,7 @@ import androidx.compose.ui.input.key.Key
|
||||
import ca.gosyer.BuildConfig
|
||||
import ca.gosyer.data.DataModule
|
||||
import ca.gosyer.ui.base.components.LoadingScreen
|
||||
import ca.gosyer.util.compose.ThemedWindow
|
||||
import ca.gosyer.ui.base.theme.AppTheme
|
||||
import ca.gosyer.util.system.userDataDir
|
||||
import com.github.zsoltk.compose.backpress.BackPressHandler
|
||||
import com.github.zsoltk.compose.backpress.LocalBackPressHandler
|
||||
@@ -112,7 +111,7 @@ fun main() {
|
||||
}
|
||||
|
||||
window.show {
|
||||
DesktopMaterialTheme {
|
||||
AppTheme {
|
||||
CompositionLocalProvider(
|
||||
LocalBackPressHandler provides backPressHandler
|
||||
) {
|
||||
|
||||
16
src/main/kotlin/ca/gosyer/util/compose/State.kt
Normal file
16
src/main/kotlin/ca/gosyer/util/compose/State.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.util.compose
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState
|
||||
|
||||
@Composable
|
||||
fun <T> State<T>.persistent(key: String) {
|
||||
val bundle = LocalSavedInstanceState.current
|
||||
}
|
||||
Reference in New Issue
Block a user