feat: graduate built-in colour picker to GA

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-08-05 00:36:44 +02:00
parent 2116c75fc1
commit 2825d71507
6 changed files with 726 additions and 330 deletions

View File

@ -262,7 +262,6 @@ dependencies {
implementation "androidx.media3:media3-ui:$media3_version"
// Compose libraries
implementation "com.github.skydoves:colorpicker-compose:1.0.5"
implementation "me.saket.telephoto:zoomable-image:1.0.0-alpha02"
implementation "me.saket.telephoto:zoomable-image-glide:1.0.0-alpha02"

View File

@ -17,24 +17,6 @@ sealed class LabsAccessControlVariates {
data class Restricted(val predicate: () -> Boolean) : LabsAccessControlVariates()
}
@FeatureFlag("BuiltInColourPicker")
sealed class BuiltInColourPickerVariates {
@Treatment(
"Use the built-in colour picker"
)
object Enabled : BuiltInColourPickerVariates()
@Treatment(
"Use the built-in colour picker for users that meet certain or all criteria (implementation-specific)"
)
data class Restricted(val predicate: () -> Boolean) : BuiltInColourPickerVariates()
@Treatment(
"Use the colour picker from the external library"
)
object Disabled : BuiltInColourPickerVariates()
}
@FeatureFlag("MediaConversations")
sealed class MediaConversationsVariates {
@Treatment(
@ -61,20 +43,6 @@ object FeatureFlags {
is LabsAccessControlVariates.Restricted -> (labsAccessControl as LabsAccessControlVariates.Restricted).predicate()
}
@FeatureFlag("BuiltInColourPicker")
var builtInColourPicker by mutableStateOf<BuiltInColourPickerVariates>(
BuiltInColourPickerVariates.Restricted {
RevoltAPI.selfId == SpecialUsers.JENNIFER
}
)
val builtInColourPickerGranted: Boolean
get() = when (builtInColourPicker) {
is BuiltInColourPickerVariates.Enabled -> true
is BuiltInColourPickerVariates.Restricted -> (builtInColourPicker as BuiltInColourPickerVariates.Restricted).predicate()
is BuiltInColourPickerVariates.Disabled -> false
}
@FeatureFlag("MediaConversations")
var mediaConversations by mutableStateOf<MediaConversationsVariates>(
MediaConversationsVariates.Restricted {

View File

@ -0,0 +1,318 @@
package chat.revolt.internals
import androidx.compose.ui.graphics.Color
// Auto-generated, do not touch
// Generator: https://gist.github.com/infi/0aea10320069f0ca71d0c49cde5ad13c
object TailwindColourScheme {
val slate = listOf(
0xFFF8FAFC,
0xFFF1F5F9,
0xFFE2E8F0,
0xFFCBD5E1,
0xFF94A3B8,
0xFF64748B,
0xFF475569,
0xFF334155,
0xFF1E293B,
0xFF0F172A,
0xFF020617,
).map { Color(it) }
val gray = listOf(
0xFFF9FAFB,
0xFFF3F4F6,
0xFFE5E7EB,
0xFFD1D5DB,
0xFF9CA3AF,
0xFF6B7280,
0xFF4B5563,
0xFF374151,
0xFF1F2937,
0xFF111827,
0xFF030712,
).map { Color(it) }
val zinc = listOf(
0xFFFAFAFA,
0xFFF4F4F5,
0xFFE4E4E7,
0xFFD4D4D8,
0xFFA1A1AA,
0xFF71717A,
0xFF52525B,
0xFF3F3F46,
0xFF27272A,
0xFF18181B,
0xFF09090B,
).map { Color(it) }
val neutral = listOf(
0xFFFAFAFA,
0xFFF5F5F5,
0xFFE5E5E5,
0xFFD4D4D4,
0xFFA3A3A3,
0xFF737373,
0xFF525252,
0xFF404040,
0xFF262626,
0xFF171717,
0xFF0A0A0A,
).map { Color(it) }
val stone = listOf(
0xFFFAFAF9,
0xFFF5F5F4,
0xFFE7E5E4,
0xFFD6D3D1,
0xFFA8A29E,
0xFF78716C,
0xFF57534E,
0xFF44403C,
0xFF292524,
0xFF1C1917,
0xFF0C0A09,
).map { Color(it) }
val red = listOf(
0xFFFEF2F2,
0xFFFEE2E2,
0xFFFECACA,
0xFFFCA5A5,
0xFFF87171,
0xFFEF4444,
0xFFDC2626,
0xFFB91C1C,
0xFF991B1B,
0xFF7F1D1D,
0xFF450A0A,
).map { Color(it) }
val orange = listOf(
0xFFFFF7ED,
0xFFFFEDD5,
0xFFFED7AA,
0xFFFDBA74,
0xFFFB923C,
0xFFF97316,
0xFFEA580C,
0xFFC2410C,
0xFF9A3412,
0xFF7C2D12,
0xFF431407,
).map { Color(it) }
val amber = listOf(
0xFFFFFBEB,
0xFFFEF3C7,
0xFFFDE68A,
0xFFFCD34D,
0xFFFBBF24,
0xFFF59E0B,
0xFFD97706,
0xFFB45309,
0xFF92400E,
0xFF78350F,
0xFF451A03,
).map { Color(it) }
val yellow = listOf(
0xFFFEFCE8,
0xFFFEF9C3,
0xFFFEF08A,
0xFFFDE047,
0xFFFACC15,
0xFFEAB308,
0xFFCA8A04,
0xFFA16207,
0xFF854D0E,
0xFF713F12,
0xFF422006,
).map { Color(it) }
val lime = listOf(
0xFFF7FEE7,
0xFFECFCCB,
0xFFD9F99D,
0xFFBEF264,
0xFFA3E635,
0xFF84CC16,
0xFF65A30D,
0xFF4D7C0F,
0xFF3F6212,
0xFF365314,
0xFF1A2E05,
).map { Color(it) }
val green = listOf(
0xFFF0FDF4,
0xFFDCFCE7,
0xFFBBF7D0,
0xFF86EFAC,
0xFF4ADE80,
0xFF22C55E,
0xFF16A34A,
0xFF15803D,
0xFF166534,
0xFF14532D,
0xFF052E16,
).map { Color(it) }
val emerald = listOf(
0xFFECFDF5,
0xFFD1FAE5,
0xFFA7F3D0,
0xFF6EE7B7,
0xFF34D399,
0xFF10B981,
0xFF059669,
0xFF047857,
0xFF065F46,
0xFF064E3B,
0xFF022C22,
).map { Color(it) }
val teal = listOf(
0xFFF0FDFA,
0xFFCCFBF1,
0xFF99F6E4,
0xFF5EEAD4,
0xFF2DD4BF,
0xFF14B8A6,
0xFF0D9488,
0xFF0F766E,
0xFF115E59,
0xFF134E4A,
0xFF042F2E,
).map { Color(it) }
val cyan = listOf(
0xFFECFEFF,
0xFFCFFAFE,
0xFFA5F3FC,
0xFF67E8F9,
0xFF22D3EE,
0xFF06B6D4,
0xFF0891B2,
0xFF0E7490,
0xFF155E75,
0xFF164E63,
0xFF083344,
).map { Color(it) }
val sky = listOf(
0xFFF0F9FF,
0xFFE0F2FE,
0xFFBAE6FD,
0xFF7DD3FC,
0xFF38BDF8,
0xFF0EA5E9,
0xFF0284C7,
0xFF0369A1,
0xFF075985,
0xFF0C4A6E,
0xFF082F49,
).map { Color(it) }
val blue = listOf(
0xFFEFF6FF,
0xFFDBEAFE,
0xFFBFDBFE,
0xFF93C5FD,
0xFF60A5FA,
0xFF3B82F6,
0xFF2563EB,
0xFF1D4ED8,
0xFF1E40AF,
0xFF1E3A8A,
0xFF172554,
).map { Color(it) }
val indigo = listOf(
0xFFEEF2FF,
0xFFE0E7FF,
0xFFC7D2FE,
0xFFA5B4FC,
0xFF818CF8,
0xFF6366F1,
0xFF4F46E5,
0xFF4338CA,
0xFF3730A3,
0xFF312E81,
0xFF1E1B4B,
).map { Color(it) }
val violet = listOf(
0xFFF5F3FF,
0xFFEDE9FE,
0xFFDDD6FE,
0xFFC4B5FD,
0xFFA78BFA,
0xFF8B5CF6,
0xFF7C3AED,
0xFF6D28D9,
0xFF5B21B6,
0xFF4C1D95,
0xFF2E1065,
).map { Color(it) }
val purple = listOf(
0xFFFAF5FF,
0xFFF3E8FF,
0xFFE9D5FF,
0xFFD8B4FE,
0xFFC084FC,
0xFFA855F7,
0xFF9333EA,
0xFF7E22CE,
0xFF6B21A8,
0xFF581C87,
0xFF3B0764,
).map { Color(it) }
val fuchsia = listOf(
0xFFFDF4FF,
0xFFFAE8FF,
0xFFF5D0FE,
0xFFF0ABFC,
0xFFE879F9,
0xFFD946EF,
0xFFC026D3,
0xFFA21CAF,
0xFF86198F,
0xFF701A75,
0xFF4A044E,
).map { Color(it) }
val pink = listOf(
0xFFFDF2F8,
0xFFFCE7F3,
0xFFFBCFE8,
0xFFF9A8D4,
0xFFF472B6,
0xFFEC4899,
0xFFDB2777,
0xFFBE185D,
0xFF9D174D,
0xFF831843,
0xFF500724,
).map { Color(it) }
val rose = listOf(
0xFFFFF1F2,
0xFFFFE4E6,
0xFFFECDD3,
0xFFFDA4AF,
0xFFFB7185,
0xFFF43F5E,
0xFFE11D48,
0xFFBE123C,
0xFF9F1239,
0xFF881337,
0xFF4C0519,
).map { Color(it) }
}

View File

@ -26,11 +26,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@ -44,17 +39,14 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection
@ -71,25 +63,17 @@ import androidx.navigation.NavController
import chat.revolt.R
import chat.revolt.api.RevoltCbor
import chat.revolt.api.RevoltJson
import chat.revolt.api.settings.FeatureFlags
import chat.revolt.api.settings.GlobalState
import chat.revolt.api.settings.SyncedSettings
import chat.revolt.components.generic.ListHeader
import chat.revolt.components.generic.SheetEnd
import chat.revolt.components.screens.settings.appearance.ColourChip
import chat.revolt.components.screens.settings.appearance.CornerRadiusPicker
import chat.revolt.internals.extensions.BottomSheetInsets
import chat.revolt.sheets.ColourPickerSheet
import chat.revolt.ui.theme.ClearRippleTheme
import chat.revolt.ui.theme.OverridableColourScheme
import chat.revolt.ui.theme.Theme
import chat.revolt.ui.theme.getFieldByName
import chat.revolt.ui.theme.systemSupportsDynamicColors
import com.github.skydoves.colorpicker.compose.AlphaSlider
import com.github.skydoves.colorpicker.compose.BrightnessSlider
import com.github.skydoves.colorpicker.compose.ColorEnvelope
import com.github.skydoves.colorpicker.compose.HsvColorPicker
import com.github.skydoves.colorpicker.compose.rememberColorPickerController
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.launch
@ -259,8 +243,9 @@ fun AppearanceSettingsScreen(
},
windowInsets = BottomSheetInsets
) {
if (FeatureFlags.builtInColourPickerGranted) {
ColourPickerSheet(initialValue = viewModel.selectedOverrideInitialValue ?: 0) {
ColourPickerSheet(
initialValue = viewModel.selectedOverrideInitialValue ?: 0,
onColourSelected = {
viewModel.updateColourOverrides(
viewModel.selectedOverrideName ?: return@ColourPickerSheet,
it
@ -269,28 +254,24 @@ fun AppearanceSettingsScreen(
sheetState.hide()
viewModel.overridePickerSheetVisible = false
}
}
} else {
ColourSelectorSheet(
initialValue = Color(viewModel.selectedOverrideInitialValue ?: 0),
onConfirm = { color ->
viewModel.updateColourOverrides(
viewModel.selectedOverrideName ?: return@ColourSelectorSheet,
color?.toArgb()
)
scope.launch {
sheetState.hide()
viewModel.overridePickerSheetVisible = false
}
},
onDismiss = {
scope.launch {
sheetState.hide()
viewModel.overridePickerSheetVisible = false
}
},
onUseDefaultColour = {
viewModel.updateColourOverrides(
viewModel.selectedOverrideName ?: return@ColourPickerSheet,
null
)
scope.launch {
sheetState.hide()
viewModel.overridePickerSheetVisible = false
}
)
}
},
onDismiss = {
scope.launch {
sheetState.hide()
viewModel.overridePickerSheetVisible = false
}
}
)
}
}
@ -535,123 +516,4 @@ fun AppearanceSettingsScreen(
}
}
}
}
@Composable
fun ColourSelectorSheet(
initialValue: Color,
onConfirm: (Color?) -> Unit,
onDismiss: () -> Unit
) {
val controller = rememberColorPickerController()
val colour = remember { mutableStateOf(initialValue) }
Column(
modifier = Modifier
.padding(20.dp)
.verticalScroll(rememberScrollState())
) {
HsvColorPicker(
modifier = Modifier
.fillMaxWidth()
.height(450.dp)
.padding(10.dp),
controller = controller,
onColorChanged = { colorEnvelope: ColorEnvelope ->
colour.value = colorEnvelope.color
},
)
AlphaSlider(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
controller = controller,
)
BrightnessSlider(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
controller = controller,
)
CompositionLocalProvider(
LocalRippleTheme provides ClearRippleTheme
) {
ColourChip(
color = colour.value,
text = "#${
(0xFFFFFF and colour.value.toArgb()).toString(16).padStart(6, '0').uppercase()
}",
modifier = Modifier
.padding(10.dp)
.fillMaxWidth()
) {}
}
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
TextButton(
onClick = {
onConfirm(null)
},
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(id = R.string.settings_appearance_colour_overrides_reset)
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextButton(
onClick = {
onDismiss()
},
modifier = Modifier.weight(1f)
) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(id = R.string.cancel)
)
}
Button(
onClick = {
onConfirm(colour.value)
},
enabled = colour.value != initialValue,
modifier = Modifier.weight(1f)
) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = null
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(id = R.string.settings_appearance_colour_overrides_apply)
)
}
}
}
}
SheetEnd()
}

View File

@ -1,20 +1,35 @@
package chat.revolt.sheets
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SegmentedButton
@ -22,21 +37,26 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.revolt.R
import chat.revolt.components.generic.SheetEnd
import chat.revolt.internals.TailwindColourScheme
enum class ColourPickerMode {
Sliders,
@ -44,42 +64,187 @@ enum class ColourPickerMode {
Hex
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> Unit) {
var selectedColour by remember { mutableIntStateOf(initialValue and 0xFFFFFF) }
private fun Color.asHsv(): Triple<Float, Float, Float> {
val max = maxOf(red, green, blue)
val min = minOf(red, green, blue)
val delta = max - min
val hue: Float = if (max == min) {
0f
} else {
when (max) {
red -> (60 * ((green - blue) / delta) + 360) % 360
green -> (60 * ((blue - red) / delta) + 120) % 360
blue -> (60 * ((red - green) / delta) + 240) % 360
else -> throw IllegalStateException("Unexpected max value: $max")
}
}
val saturation: Float = if (max == 0f) 0f else delta / max
return Triple(hue, saturation, max)
}
private fun Color.asHexString(): String {
val alpha = (alpha * 255).toInt()
val red = (red * 255).toInt()
val green = (green * 255).toInt()
val blue = (blue * 255).toInt()
return String.format("#%02X%02X%02X%02X", alpha, red, green, blue)
}
// 11x11 palette of Tailwind colours
val palette = listOf(
TailwindColourScheme.neutral[0],
TailwindColourScheme.red[0],
TailwindColourScheme.red[1],
TailwindColourScheme.red[2],
TailwindColourScheme.red[3],
TailwindColourScheme.red[4],
TailwindColourScheme.red[5],
TailwindColourScheme.red[6],
TailwindColourScheme.red[7],
TailwindColourScheme.red[9],
TailwindColourScheme.red[10],
TailwindColourScheme.neutral[1],
TailwindColourScheme.orange[0],
TailwindColourScheme.orange[1],
TailwindColourScheme.orange[2],
TailwindColourScheme.orange[3],
TailwindColourScheme.orange[4],
TailwindColourScheme.orange[5],
TailwindColourScheme.orange[6],
TailwindColourScheme.orange[7],
TailwindColourScheme.orange[9],
TailwindColourScheme.orange[10],
TailwindColourScheme.neutral[2],
TailwindColourScheme.yellow[0],
TailwindColourScheme.yellow[1],
TailwindColourScheme.yellow[2],
TailwindColourScheme.yellow[3],
TailwindColourScheme.yellow[4],
TailwindColourScheme.yellow[5],
TailwindColourScheme.yellow[6],
TailwindColourScheme.yellow[7],
TailwindColourScheme.yellow[9],
TailwindColourScheme.yellow[10],
TailwindColourScheme.neutral[3],
TailwindColourScheme.green[0],
TailwindColourScheme.green[1],
TailwindColourScheme.green[2],
TailwindColourScheme.green[3],
TailwindColourScheme.green[4],
TailwindColourScheme.green[5],
TailwindColourScheme.green[6],
TailwindColourScheme.green[7],
TailwindColourScheme.green[9],
TailwindColourScheme.green[10],
TailwindColourScheme.neutral[4],
TailwindColourScheme.teal[0],
TailwindColourScheme.teal[1],
TailwindColourScheme.teal[2],
TailwindColourScheme.teal[3],
TailwindColourScheme.teal[4],
TailwindColourScheme.teal[5],
TailwindColourScheme.teal[6],
TailwindColourScheme.teal[7],
TailwindColourScheme.teal[9],
TailwindColourScheme.teal[10],
TailwindColourScheme.neutral[5],
TailwindColourScheme.sky[0],
TailwindColourScheme.sky[1],
TailwindColourScheme.sky[2],
TailwindColourScheme.sky[3],
TailwindColourScheme.sky[4],
TailwindColourScheme.sky[5],
TailwindColourScheme.sky[6],
TailwindColourScheme.sky[7],
TailwindColourScheme.sky[9],
TailwindColourScheme.sky[10],
TailwindColourScheme.neutral[6],
TailwindColourScheme.blue[0],
TailwindColourScheme.blue[1],
TailwindColourScheme.blue[2],
TailwindColourScheme.blue[3],
TailwindColourScheme.blue[4],
TailwindColourScheme.blue[5],
TailwindColourScheme.blue[6],
TailwindColourScheme.blue[7],
TailwindColourScheme.blue[9],
TailwindColourScheme.blue[10],
TailwindColourScheme.neutral[7],
TailwindColourScheme.violet[0],
TailwindColourScheme.violet[1],
TailwindColourScheme.violet[2],
TailwindColourScheme.violet[3],
TailwindColourScheme.violet[4],
TailwindColourScheme.violet[5],
TailwindColourScheme.violet[6],
TailwindColourScheme.violet[7],
TailwindColourScheme.violet[9],
TailwindColourScheme.violet[10],
TailwindColourScheme.neutral[8],
TailwindColourScheme.purple[0],
TailwindColourScheme.purple[1],
TailwindColourScheme.purple[2],
TailwindColourScheme.purple[3],
TailwindColourScheme.purple[4],
TailwindColourScheme.purple[5],
TailwindColourScheme.purple[6],
TailwindColourScheme.purple[7],
TailwindColourScheme.purple[9],
TailwindColourScheme.purple[10],
TailwindColourScheme.neutral[9],
TailwindColourScheme.pink[0],
TailwindColourScheme.pink[1],
TailwindColourScheme.pink[2],
TailwindColourScheme.pink[3],
TailwindColourScheme.pink[4],
TailwindColourScheme.pink[5],
TailwindColourScheme.pink[6],
TailwindColourScheme.pink[7],
TailwindColourScheme.pink[9],
TailwindColourScheme.pink[10],
TailwindColourScheme.neutral[10],
TailwindColourScheme.rose[0],
TailwindColourScheme.rose[1],
TailwindColourScheme.rose[2],
TailwindColourScheme.rose[3],
TailwindColourScheme.rose[4],
TailwindColourScheme.rose[5],
TailwindColourScheme.rose[6],
TailwindColourScheme.rose[7],
TailwindColourScheme.rose[9],
TailwindColourScheme.rose[10],
)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
@Composable
fun ColumnScope.ColourPickerSheet(
initialValue: Int,
onColourSelected: (Int) -> Unit,
onUseDefaultColour: () -> Unit,
onDismiss: () -> Unit
) {
var color by remember { mutableStateOf(Color(initialValue)) }
var mode by remember { mutableStateOf(ColourPickerMode.Sliders) }
val hueComponent by remember(selectedColour) { derivedStateOf { selectedColour shr 16 and 0xFF } }
val saturationComponent by remember(selectedColour) { derivedStateOf { selectedColour shr 8 and 0xFF } }
val valueComponent by remember(selectedColour) { derivedStateOf { selectedColour and 0xFF } }
val hueTrackColours = remember {
(0..255).map {
Color(
android.graphics.Color.HSVToColor(
floatArrayOf(
it.toFloat(),
1f,
1f
)
)
)
(0..359).map {
Color.hsv(it.toFloat(), 1f, 1f, 1f)
}
}
var pendingHexColourString by remember(selectedColour) {
// First we convert the colour from HHSSVV to #RRGGBB.
val asRgb = android.graphics.Color.HSVToColor(
floatArrayOf(
hueComponent.toFloat(),
saturationComponent / 255f,
valueComponent / 255f
)
)
mutableStateOf("#${asRgb.toString(16).padStart(6, '0')}")
}
val colorHsv by remember(color) { derivedStateOf { color.asHsv() } }
Column(
Modifier
@ -132,7 +297,10 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
style = MaterialTheme.typography.labelLarge
)
Text(
hueComponent.toString(),
stringResource(
R.string.colour_picker_hue_value_fmt,
colorHsv.first.toInt()
),
style = MaterialTheme.typography.labelLarge.copy(
color = MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.6f
@ -142,22 +310,17 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
}
Slider(
value = hueComponent.toFloat(),
value = colorHsv.first,
onValueChange = {
selectedColour =
(selectedColour and 0x00FFFF) or (it.toInt() shl 16)
val (_, saturation, value) = colorHsv
color = Color.hsv(it, saturation, value)
},
valueRange = 0f..255f,
valueRange = 0f..359f,
colors = SliderDefaults.colors().copy(
// The thumb colour is the current hue at full saturation and value.
thumbColor = Color(
android.graphics.Color.HSVToColor(
floatArrayOf(
hueComponent.toFloat(),
1f,
1f
)
)
thumbColor = Color.hsv(
colorHsv.first,
1f,
1f
)
),
track = {
@ -167,7 +330,10 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
.height(4.dp)
) {
drawRect(
Brush.horizontalGradient(hueTrackColours, endX = size.width)
Brush.horizontalGradient(
hueTrackColours,
endX = size.width
)
)
}
}
@ -182,7 +348,7 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
style = MaterialTheme.typography.labelLarge
)
Text(
saturationComponent.toString(),
(colorHsv.second * 100).toInt().toString(),
style = MaterialTheme.typography.labelLarge.copy(
color = MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.6f
@ -192,21 +358,17 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
}
Slider(
value = saturationComponent.toFloat(),
value = colorHsv.second,
onValueChange = {
selectedColour =
(selectedColour and 0xFF00FF) or (it.toInt() shl 8)
val (hue, _, value) = colorHsv
color = Color.hsv(hue, it, value)
},
valueRange = 0f..255f,
valueRange = 0f..1f,
colors = SliderDefaults.colors().copy(
thumbColor = Color(
android.graphics.Color.HSVToColor(
floatArrayOf(
hueComponent.toFloat(),
(selectedColour shr 8 and 0xFF) / 255f,
1f
)
)
thumbColor = Color.hsv(
colorHsv.first,
colorHsv.second,
1f
)
),
track = {
@ -218,23 +380,15 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
drawRect(
Brush.horizontalGradient(
listOf(
Color(
android.graphics.Color.HSVToColor(
floatArrayOf(
hueComponent.toFloat(),
0f,
1f
)
)
Color.hsv(
colorHsv.first,
0f,
1f
),
Color(
android.graphics.Color.HSVToColor(
floatArrayOf(
hueComponent.toFloat(),
1f,
1f
)
)
Color.hsv(
colorHsv.first,
1f,
1f
)
),
endX = size.width
@ -253,7 +407,7 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
style = MaterialTheme.typography.labelLarge
)
Text(
valueComponent.toString(),
(colorHsv.third * 100).toInt().toString(),
style = MaterialTheme.typography.labelLarge.copy(
color = MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.6f
@ -263,21 +417,17 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
}
Slider(
value = valueComponent.toFloat(),
value = colorHsv.third,
onValueChange = {
selectedColour =
(selectedColour and 0xFFFF00) or it.toInt()
val (hue, saturation, _) = colorHsv
color = Color.hsv(hue, saturation, it)
},
valueRange = 0f..255f,
valueRange = 0f..1f,
colors = SliderDefaults.colors().copy(
thumbColor = Color(
android.graphics.Color.HSVToColor(
floatArrayOf(
hueComponent.toFloat(),
(selectedColour shr 8 and 0xFF) / 255f,
(selectedColour and 0xFF) / 255f
)
)
thumbColor = Color.hsv(
colorHsv.first,
colorHsv.second,
colorHsv.third
)
),
track = {
@ -289,23 +439,15 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
drawRect(
Brush.horizontalGradient(
listOf(
Color(
android.graphics.Color.HSVToColor(
floatArrayOf(
hueComponent.toFloat(),
(selectedColour shr 8 and 0xFF) / 255f,
0f
)
)
Color.hsv(
colorHsv.first,
colorHsv.second,
0f
),
Color(
android.graphics.Color.HSVToColor(
floatArrayOf(
hueComponent.toFloat(),
(selectedColour shr 8 and 0xFF) / 255f,
1f
)
)
Color.hsv(
colorHsv.first,
colorHsv.second,
1f
)
),
endX = size.width
@ -314,36 +456,120 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
}
}
)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier.fillMaxWidth()
) {
Text(
stringResource(R.string.colour_picker_alpha),
style = MaterialTheme.typography.labelLarge
)
Text(
(color.alpha * 100).toInt().toString(),
style = MaterialTheme.typography.labelLarge.copy(
color = MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.6f
)
)
)
}
Slider(
value = color.alpha,
onValueChange = { color = color.copy(alpha = it) },
valueRange = 0f..1f,
colors = SliderDefaults.colors().copy(
thumbColor = color
),
track = {
Canvas(
Modifier
.fillMaxWidth()
.height(4.dp)
) {
drawRect(
Brush.horizontalGradient(
listOf(
color.copy(alpha = 0f),
color.copy(alpha = 1f)
),
endX = size.width
)
)
}
}
)
}
}
ColourPickerMode.Palette -> {
Text("TODO: Palette picker", Modifier.fillMaxWidth())
BoxWithConstraints {
val boxMaxWidth = this.maxWidth
FlowRow(
maxItemsInEachRow = 11,
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
for (colour in palette) {
Box(
Modifier
.clip(CircleShape)
.clickable { color = colour }
.size((boxMaxWidth - 80.dp) / 11)
.background(colour)
)
}
}
}
}
ColourPickerMode.Hex -> {
OutlinedTextField(
value = pendingHexColourString,
onValueChange = {
pendingHexColourString = it
var hex by remember(color) {
mutableStateOf(color.asHexString())
}
var isFocused by remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current
if ("#[0-9a-fA-F]{6}".toRegex().matches(it)) {
val newColour =
it.substring(1).toIntOrNull(16) ?: return@OutlinedTextField
val floatArr = FloatArray(3)
android.graphics.Color.RGBToHSV(
newColour shr 16 and 0xFF,
newColour shr 8 and 0xFF,
newColour and 0xFF,
floatArr
)
selectedColour = floatArr.fold(0) { acc, f ->
(acc shl 8) or (f.toInt() and 0xFF)
BackHandler(enabled = isFocused) {
focusManager.clearFocus()
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedTextField(
value = hex,
onValueChange = {
hex = if (it.isNotEmpty() && it[0] == '#') {
it
} else {
"#$it"
}
}
},
modifier = Modifier.fillMaxWidth()
)
},
label = { Text(stringResource(R.string.colour_picker_hex_template)) },
modifier = Modifier
.fillMaxWidth()
.onFocusChanged {
isFocused = it.isFocused
},
)
TextButton(
onClick = {
try {
color = Color(android.graphics.Color.parseColor(hex))
} catch (e: IllegalArgumentException) {
// Ignore
}
},
enabled = (hex.length == 9 || hex.length == 7) && hex[0] == '#' && hex.substring(
1
).all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' },
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.colour_picker_hex_use))
}
}
}
}
}
@ -357,21 +583,43 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
Canvas(
Modifier
.clip(MaterialTheme.shapes.large)
.fillMaxWidth()
.height(64.dp)
) {
drawRect(
Color(
android.graphics.Color.HSVToColor(
floatArrayOf(
hueComponent.toFloat(),
saturationComponent / 255f,
valueComponent / 255f
)
)
),
size = size
)
drawRect(color)
}
Spacer(Modifier.height(8.dp))
Column(
Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
TextButton(onClick = onUseDefaultColour, Modifier.fillMaxWidth()) {
Icon(Icons.Default.CheckCircle, null)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.colour_picker_use_default))
}
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp)
) {
TextButton(onClick = onDismiss, modifier = Modifier.weight(1f)) {
Icon(Icons.Default.Close, null)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.colour_picker_cancel))
}
Button(
onClick = { onColourSelected(color.toArgb()) },
modifier = Modifier.weight(1f)
) {
Icon(Icons.Default.Check, null)
Spacer(Modifier.width(8.dp))
Text(stringResource(R.string.colour_picker_apply))
}
}
}
}

View File

@ -456,15 +456,18 @@
<string name="colour_picker_mode_sliders">Sliders</string>
<string name="colour_picker_mode_palette">Palette</string>
<string name="colour_picker_hue">Hue</string>
<string name="colour_picker_hue_value_fmt">%1$d°</string>
<string name="colour_picker_saturation">Saturation</string>
<string name="colour_picker_value">Value</string>
<string name="colour_picker_red">Red</string>
<string name="colour_picker_green">Green</string>
<string name="colour_picker_blue">Blue</string>
<string name="colour_picker_hex">Hex</string>
<string name="colour_picker_hex_template" translatable="false">#AARRGGBB</string>
<string name="colour_picker_hex_use">Use</string>
<string name="colour_picker_alpha">Alpha</string>
<string name="colour_picker_preview">Preview</string>
<string name="colour_picker_reset_to_default">Reset to default</string>
<string name="colour_picker_use_default">Default</string>
<string name="colour_picker_cancel">Cancel</string>
<string name="colour_picker_apply">Apply</string>
@ -581,8 +584,6 @@
<string name="settings_appearance_colour_overrides_outline">Outline</string>
<string name="settings_appearance_colour_overrides_outline_variant">Outline Variant</string>
<string name="settings_appearance_colour_overrides_scrim">Scrim</string>
<string name="settings_appearance_colour_overrides_apply">Apply</string>
<string name="settings_appearance_colour_overrides_reset">Reset</string>
<string name="settings_appearance_colour_overrides_export">Export</string>
<string name="settings_appearance_colour_overrides_import">Import</string>
<string name="settings_appearance_colour_overrides_import_error">This file is not a valid colour override file.</string>