feat(IconOpts): allow entering hex RGB colors

This commit is contained in:
rushiiMachine 2025-03-30 16:06:37 -07:00
parent 41de5c8b56
commit c22567f337
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
4 changed files with 114 additions and 12 deletions

View file

@ -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),
)
}
}
}

View file

@ -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),
)
}

View file

@ -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<Color, Long>(
save = { it.value.toLong() },
restore = { Color(it.toULong()) },
)

View file

@ -169,6 +169,12 @@
<string name="iconopts_variant_desc_discord">Does not alter the original icon, instead only adding the missing monochrome icon to the APK.</string>
<string name="iconopts_variant_desc_aliucord">Changes the original icon color to Aliucord\'s branding.</string>
<string name="iconopts_variant_desc_color">Select a custom icon background color.</string>
<string name="iconopts_colorpicker_title">Hue &amp; Saturation</string>
<string name="iconopts_colorpicker_desc">The main HSV color components.</string>
<string name="iconopts_lightness_title">Lightness</string>
<string name="iconopts_lightness_desc">The brightness component of the color.</string>
<string name="iconopts_hex_title">Hex Color</string>
<string name="iconopts_hex_desc">Manually enter the RGB hex value of the color. Transparency is ignored!</string>
<string name="notif_group_install_title">Active installation</string>
<string name="notif_group_install_desc">Progress notifications sent during a minimized installation</string>