From c22567f337af559a5a5942b42d36e260f8d1a3e0 Mon Sep 17 00:00:00 2001 From: rushiiMachine <33725716+rushiiMachine@users.noreply.github.com> Date: Sun, 30 Mar 2025 16:06:37 -0700 Subject: [PATCH] feat(IconOpts): allow entering hex RGB colors --- .../ui/screens/iconopts/IconOptionsScreen.kt | 48 ++++++++++---- .../iconopts/components/RGBTextField.kt | 62 +++++++++++++++++++ .../com/aliucord/manager/ui/util/Utils.kt | 10 +++ app/src/main/res/values/strings.xml | 6 ++ 4 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/components/RGBTextField.kt 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 d503c1f..d8eb98e 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 @@ -7,8 +7,8 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -18,10 +18,11 @@ 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.iconopts.components.* import com.aliucord.manager.ui.screens.patchopts.components.options.PatchOption +import com.aliucord.manager.ui.util.ColorSaver import dev.zt64.compose.pipette.CircularColorPicker +import dev.zt64.compose.pipette.util.* import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @@ -109,15 +110,19 @@ fun IconOptionsScreenContent( @Composable private fun CustomColorOptions(color: HSVColorState) { + // This color is separated from the live color and intentionally lags behind while the RGBTextField is being edited. + // When this changes, then the text inside the RGBTextField is reset to the fully formatted color. + // As such, this only happens when the color is changed via the other color pickers. + var initialRGBFieldColor by rememberSaveable(stateSaver = ColorSaver) { mutableStateOf(color.toARGB()) } + 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() + name = stringResource(R.string.iconopts_colorpicker_title), + description = stringResource(R.string.iconopts_colorpicker_desc), + modifier = Modifier.fillMaxWidth(), ) { CircularColorPicker( hue = color.hue, @@ -126,21 +131,25 @@ private fun CustomColorOptions(color: HSVColorState) { onColorChange = { hue, saturation -> color.hue = hue color.saturation = saturation + initialRGBFieldColor = color.toARGB() }, modifier = Modifier - .padding(top = 20.dp) + .padding(top = 12.dp) .size(260.dp) .align(Alignment.CenterHorizontally) ) } PatchOption( - name = "Lightness", - description = "The brightness component of the color", + name = stringResource(R.string.iconopts_lightness_title), + description = stringResource(R.string.iconopts_lightness_desc), ) { InteractiveSlider( value = color.value, - onValueChange = { color.value = it }, + onValueChange = { + color.value = it + initialRGBFieldColor = color.toARGB() + }, valueRange = 0f..1f, brush = Brush.horizontalGradient( listOf( @@ -151,5 +160,20 @@ private fun CustomColorOptions(color: HSVColorState) { thumbColor = color.toARGB(), ) } + + PatchOption( + name = stringResource(R.string.iconopts_hex_title), + description = stringResource(R.string.iconopts_hex_desc), + ) { + RGBTextField( + initialColor = initialRGBFieldColor, + setColor = { + color.hue = it.hue + color.saturation = it.saturation + color.value = it.hsvValue + }, + modifier = Modifier.padding(top = 12.dp), + ) + } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/components/RGBTextField.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/components/RGBTextField.kt new file mode 100644 index 0000000..5ed8f7a --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/iconopts/components/RGBTextField.kt @@ -0,0 +1,62 @@ +package com.aliucord.manager.ui.screens.iconopts.components + +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.unit.dp +import java.util.Locale +import kotlin.math.roundToInt + +private fun formatRGBComponent(value: Float): String { + return (value * 255).roundToInt() + .toString(16) + .padStart(2, '0') +} + +private fun formatRGBColor(color: Color): String { + return (formatRGBComponent(color.red) + formatRGBComponent(color.green) + formatRGBComponent(color.blue)) + .uppercase(Locale.ENGLISH) +} + +@Composable +fun RGBTextField( + initialColor: Color, + setColor: (Color) -> Unit, + modifier: Modifier = Modifier, +) { + var isError by rememberSaveable { mutableStateOf(false) } + var text by rememberSaveable(initialColor) { mutableStateOf(formatRGBColor(initialColor)) } + + // Clear focus from TextField if the starting color has changed + val focusManager = LocalFocusManager.current + DisposableEffect(initialColor) { + focusManager.clearFocus() + + onDispose {} + } + + TextField( + value = text, + onValueChange = { newText -> + val cleaned = newText.removePrefix("#").uppercase() + val newColor = cleaned.toLongOrNull(16)?.let(::Color)?.copy(alpha = 1f) + + text = cleaned + isError = newColor == null + newColor?.let(setColor) + }, + isError = isError, + singleLine = true, + keyboardOptions = KeyboardOptions(autoCorrectEnabled = false), + label = { Text("Hex Value") }, + prefix = { Text("# ") }, + modifier = modifier + .widthIn(min = 200.dp, max = 300.dp), + ) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/util/Utils.kt b/app/src/main/kotlin/com/aliucord/manager/ui/util/Utils.kt index 6e3d4f3..15eb107 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/util/Utils.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/util/Utils.kt @@ -5,8 +5,10 @@ package com.aliucord.manager.ui.util import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.saveable.Saver import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.* import kotlinx.collections.immutable.ImmutableList @@ -64,3 +66,11 @@ inline fun annotatingStringResource( @Stable inline fun Modifier.mirrorVertically(): Modifier = scale(scaleX = -1f, scaleY = 1f) + +/** + * Allow using compose [Color] in [androidx.compose.runtime.saveable.rememberSaveable] + */ +val ColorSaver = Saver( + save = { it.value.toLong() }, + restore = { Color(it.toULong()) }, +) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 115dbca..a8d27a3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -169,6 +169,12 @@ Does not alter the original icon, instead only adding the missing monochrome icon to the APK. Changes the original icon color to Aliucord\'s branding. Select a custom icon background color. + Hue & Saturation + The main HSV color components. + Lightness + The brightness component of the color. + Hex Color + Manually enter the RGB hex value of the color. Transparency is ignored! Active installation Progress notifications sent during a minimized installation