feat(IconOpts): add lightness slider & improve ui
This commit is contained in:
parent
b5beb8608f
commit
6427c19783
6 changed files with 212 additions and 25 deletions
|
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2025 zt64
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
// Adapted from compose-pipette:
|
||||
// https://github.com/zt64/compose-pipette/blob/3e9fd958a315dceb142bf30250b4614ecde4e723/sample/src/commonMain/kotlin/dev/zt64/compose/pipette/sample/SampleSlider.kt
|
||||
|
||||
package com.aliucord.manager.ui.components
|
||||
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.interaction.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.center
|
||||
import androidx.compose.ui.graphics.*
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun InteractiveSlider(
|
||||
value: Float,
|
||||
onValueChange: (Float) -> Unit,
|
||||
valueRange: ClosedFloatingPointRange<Float>,
|
||||
brush: Brush,
|
||||
thumbColor: Color = MaterialTheme.colorScheme.primary,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val interactionSource = remember(::MutableInteractionSource)
|
||||
|
||||
Slider(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
thumb = {
|
||||
val interactions = remember { mutableStateListOf<Interaction>() }
|
||||
|
||||
LaunchedEffect(interactionSource) {
|
||||
interactionSource.interactions.collect { interaction ->
|
||||
when (interaction) {
|
||||
is PressInteraction.Press -> interactions.add(interaction)
|
||||
is PressInteraction.Release -> interactions.remove(interaction.press)
|
||||
is PressInteraction.Cancel -> interactions.remove(interaction.press)
|
||||
is DragInteraction.Start -> interactions.add(interaction)
|
||||
is DragInteraction.Stop -> interactions.remove(interaction.start)
|
||||
is DragInteraction.Cancel -> interactions.remove(interaction.start)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(28.dp)
|
||||
.hoverable(interactionSource = interactionSource),
|
||||
) {
|
||||
val visualSize by remember {
|
||||
derivedStateOf {
|
||||
if (interactions.isNotEmpty()) 28.dp else 24.dp
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.size(visualSize)
|
||||
.align(Alignment.Center)
|
||||
.background(thumbColor, CircleShape),
|
||||
)
|
||||
}
|
||||
},
|
||||
track = {
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.widthIn(max = 700.dp)
|
||||
.height(12.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
drawLine(
|
||||
brush = brush,
|
||||
start = Offset(0f, size.center.y),
|
||||
end = Offset(size.width, size.center.y),
|
||||
strokeWidth = size.height,
|
||||
cap = StrokeCap.Round,
|
||||
)
|
||||
}
|
||||
},
|
||||
valueRange = valueRange,
|
||||
interactionSource = interactionSource,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
package com.aliucord.manager.ui.screens.home.components
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
|
|
@ -73,6 +72,7 @@ fun InstallButton(
|
|||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(end = 10.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_add),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package com.aliucord.manager.ui.screens.iconopts
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import dev.zt64.compose.pipette.util.*
|
||||
|
||||
@Stable
|
||||
class HSVColorState(
|
||||
hue: Float,
|
||||
saturation: Float,
|
||||
value: Float,
|
||||
) {
|
||||
var hue by mutableFloatStateOf(hue)
|
||||
var saturation by mutableFloatStateOf(saturation)
|
||||
var value by mutableFloatStateOf(value)
|
||||
|
||||
fun toARGB(): Color = Color.hsv(hue, saturation, value)
|
||||
override fun toString(): String = "HSVColorState($hue, $saturation, $value)"
|
||||
}
|
||||
|
||||
fun Color.toHSVState(): HSVColorState {
|
||||
return HSVColorState(
|
||||
hue = this.hue,
|
||||
saturation = this.saturation,
|
||||
value = this.hsvValue,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.aliucord.manager.ui.screens.iconopts
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
enum class IconOptionsMode {
|
||||
Original,
|
||||
Aliucord,
|
||||
CustomColor,
|
||||
CustomImage,
|
||||
}
|
||||
|
|
@ -17,12 +17,11 @@ class IconOptionsModel(
|
|||
}
|
||||
|
||||
// ---------- Replacement color ---------- //
|
||||
var selectedColorValue by mutableLongStateOf(0)
|
||||
val selectedColor: Color
|
||||
inline get() = Color(selectedColorValue.toULong())
|
||||
var selectedColor by mutableStateOf<HSVColorState>(IconReplacement.Aliucord.color.toHSVState())
|
||||
private set
|
||||
|
||||
fun changeSelectedColor(color: Color) {
|
||||
selectedColorValue = color.value.toLong()
|
||||
fun initSelectedColor(color: Color) {
|
||||
selectedColor = color.toHSVState()
|
||||
}
|
||||
|
||||
// ---------- Other ---------- //
|
||||
|
|
@ -30,12 +29,12 @@ class IconOptionsModel(
|
|||
when (prefilledOptions) {
|
||||
is IconReplacement.CustomColor if prefilledOptions.color == IconReplacement.Aliucord.color -> {
|
||||
changeMode(IconOptionsMode.Aliucord)
|
||||
changeSelectedColor(IconReplacement.Aliucord.color)
|
||||
initSelectedColor(IconReplacement.Aliucord.color)
|
||||
}
|
||||
|
||||
is IconReplacement.CustomColor -> {
|
||||
changeMode(IconOptionsMode.CustomColor)
|
||||
changeSelectedColor(prefilledOptions.color)
|
||||
initSelectedColor(prefilledOptions.color)
|
||||
}
|
||||
|
||||
is IconReplacement.CustomImage -> {
|
||||
|
|
@ -50,14 +49,7 @@ class IconOptionsModel(
|
|||
fun generateConfig(): IconReplacement = when (mode) {
|
||||
IconOptionsMode.Original -> IconReplacement.Original
|
||||
IconOptionsMode.Aliucord -> IconReplacement.Aliucord
|
||||
IconOptionsMode.CustomColor -> IconReplacement.CustomColor(color = selectedColor)
|
||||
IconOptionsMode.CustomColor -> IconReplacement.CustomColor(color = selectedColor.toARGB())
|
||||
IconOptionsMode.CustomImage -> TODO()
|
||||
}
|
||||
}
|
||||
|
||||
enum class IconOptionsMode {
|
||||
Original,
|
||||
Aliucord,
|
||||
CustomColor,
|
||||
CustomImage,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,16 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.ui.components.InteractiveSlider
|
||||
import com.aliucord.manager.ui.screens.iconopts.components.IconOptionsAppBar
|
||||
import com.aliucord.manager.ui.screens.iconopts.components.RadioSelectorItem
|
||||
import com.aliucord.manager.ui.screens.patchopts.components.options.PatchOption
|
||||
import dev.zt64.compose.pipette.CircularColorPicker
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
|
@ -46,7 +49,6 @@ class IconOptionsScreen(
|
|||
mode = model.mode,
|
||||
setMode = model::changeMode,
|
||||
selectedColor = model.selectedColor,
|
||||
setSelectedColor = model::changeSelectedColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -55,8 +57,7 @@ class IconOptionsScreen(
|
|||
fun IconOptionsScreenContent(
|
||||
mode: IconOptionsMode,
|
||||
setMode: (IconOptionsMode) -> Unit,
|
||||
selectedColor: Color,
|
||||
setSelectedColor: (Color) -> Unit,
|
||||
selectedColor: HSVColorState,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = { IconOptionsAppBar() },
|
||||
|
|
@ -100,12 +101,55 @@ fun IconOptionsScreenContent(
|
|||
enter = fadeIn() + slideInVertically(),
|
||||
exit = fadeOut() + slideOutVertically { -it / 10 },
|
||||
) {
|
||||
CircularColorPicker(
|
||||
color = selectedColor,
|
||||
onColorChange = setSelectedColor,
|
||||
modifier = Modifier.size(230.dp),
|
||||
)
|
||||
CustomColorOptions(selectedColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CustomColorOptions(color: HSVColorState) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(30.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = Modifier.padding(horizontal = 15.dp)
|
||||
) {
|
||||
PatchOption(
|
||||
name = "Hue & Saturation",
|
||||
description = "The main color components",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
CircularColorPicker(
|
||||
hue = color.hue,
|
||||
saturation = color.saturation,
|
||||
value = color.value,
|
||||
onColorChange = { hue, saturation ->
|
||||
color.hue = hue
|
||||
color.saturation = saturation
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(top = 20.dp)
|
||||
.size(260.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
|
||||
PatchOption(
|
||||
name = "Lightness",
|
||||
description = "The brightness component of the color",
|
||||
) {
|
||||
InteractiveSlider(
|
||||
value = color.value,
|
||||
onValueChange = { color.value = it },
|
||||
valueRange = 0f..1f,
|
||||
brush = Brush.horizontalGradient(
|
||||
listOf(
|
||||
Color.Black,
|
||||
Color.hsv(color.hue, color.saturation, 1f)
|
||||
),
|
||||
),
|
||||
thumbColor = color.toARGB(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue