From 6427c197834193cc33d5607391c28c72da588522 Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Sun, 30 Mar 2025 00:25:53 -0700 Subject: [PATCH] feat(IconOpts): add lightness slider & improve ui --- .../ui/components/InteractiveSlider.kt | 113 ++++++++++++++++++ .../screens/home/components/InstallButton.kt | 4 +- .../ui/screens/iconopts/HSVColorState.kt | 27 +++++ .../ui/screens/iconopts/IconOptionsMode.kt | 11 ++ .../ui/screens/iconopts/IconOptionsModel.kt | 22 ++-- .../ui/screens/iconopts/IconOptionsScreen.kt | 60 ++++++++-- 6 files changed, 212 insertions(+), 25 deletions(-) create mode 100644 app/src/main/kotlin/com/aliucord/manager/ui/components/InteractiveSlider.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/HSVColorState.kt create mode 100644 app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/IconOptionsMode.kt diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/InteractiveSlider.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/InteractiveSlider.kt new file mode 100644 index 0000000..5fc5e9c --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/InteractiveSlider.kt @@ -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, + brush: Brush, + thumbColor: Color = MaterialTheme.colorScheme.primary, + modifier: Modifier = Modifier, +) { + val interactionSource = remember(::MutableInteractionSource) + + Slider( + value = value, + onValueChange = onValueChange, + thumb = { + val interactions = remember { mutableStateListOf() } + + 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, + ) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstallButton.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstallButton.kt index 3e37d26..47016c1 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstallButton.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/home/components/InstallButton.kt @@ -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), diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/HSVColorState.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/HSVColorState.kt new file mode 100644 index 0000000..b59489b --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/HSVColorState.kt @@ -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, + ) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/IconOptionsMode.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/IconOptionsMode.kt new file mode 100644 index 0000000..68b705e --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/IconOptionsMode.kt @@ -0,0 +1,11 @@ +package com.aliucord.manager.ui.screens.iconopts + +import androidx.compose.runtime.Immutable + +@Immutable +enum class IconOptionsMode { + Original, + Aliucord, + CustomColor, + CustomImage, +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/IconOptionsModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/IconOptionsModel.kt index a260b4a..955671a 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/IconOptionsModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/IconOptionsModel.kt @@ -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(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, -} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/IconOptionsScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/IconOptionsScreen.kt index f03f53d..d503c1f 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/IconOptionsScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/IconOptionsScreen.kt @@ -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(), + ) + } + } +}