feat: experimental colour picker (very broken, featureflag)
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
b5f8e0ceee
commit
d6b75e937f
|
|
@ -17,6 +17,24 @@ 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(
|
||||
|
|
@ -43,6 +61,20 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ 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
|
||||
|
|
@ -78,6 +79,7 @@ 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
|
||||
|
|
@ -257,25 +259,38 @@ fun AppearanceSettingsScreen(
|
|||
},
|
||||
windowInsets = BottomSheetInsets
|
||||
) {
|
||||
ColourSelectorSheet(
|
||||
initialValue = Color(viewModel.selectedOverrideInitialValue ?: 0),
|
||||
onConfirm = { color ->
|
||||
if (FeatureFlags.builtInColourPickerGranted) {
|
||||
ColourPickerSheet(initialValue = viewModel.selectedOverrideInitialValue ?: 0) {
|
||||
viewModel.updateColourOverrides(
|
||||
viewModel.selectedOverrideName ?: return@ColourSelectorSheet,
|
||||
color?.toArgb()
|
||||
viewModel.selectedOverrideName ?: return@ColourPickerSheet,
|
||||
it
|
||||
)
|
||||
scope.launch {
|
||||
sheetState.hide()
|
||||
viewModel.overridePickerSheetVisible = false
|
||||
}
|
||||
},
|
||||
onDismiss = {
|
||||
scope.launch {
|
||||
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
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,379 @@
|
|||
package chat.revolt.sheets
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.CornerSize
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Text
|
||||
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.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.revolt.R
|
||||
import chat.revolt.components.generic.SheetEnd
|
||||
|
||||
enum class ColourPickerMode {
|
||||
Sliders,
|
||||
Palette,
|
||||
Hex
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> Unit) {
|
||||
var selectedColour by remember { mutableIntStateOf(initialValue and 0xFFFFFF) }
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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')}")
|
||||
}
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
SingleChoiceSegmentedButtonRow(Modifier.fillMaxWidth()) {
|
||||
SegmentedButton(
|
||||
selected = mode == ColourPickerMode.Sliders,
|
||||
onClick = { mode = ColourPickerMode.Sliders },
|
||||
shape = CircleShape.copy(
|
||||
topEnd = CornerSize(0),
|
||||
bottomEnd = CornerSize(0)
|
||||
)
|
||||
) {
|
||||
Text(stringResource(R.string.colour_picker_mode_sliders))
|
||||
}
|
||||
SegmentedButton(
|
||||
selected = mode == ColourPickerMode.Palette,
|
||||
onClick = { mode = ColourPickerMode.Palette },
|
||||
shape = RectangleShape
|
||||
) {
|
||||
Text(stringResource(R.string.colour_picker_mode_palette))
|
||||
}
|
||||
SegmentedButton(
|
||||
selected = mode == ColourPickerMode.Hex,
|
||||
onClick = { mode = ColourPickerMode.Hex },
|
||||
shape = CircleShape.copy(
|
||||
topStart = CornerSize(0),
|
||||
bottomStart = CornerSize(0)
|
||||
)
|
||||
) {
|
||||
Text(stringResource(R.string.colour_picker_hex))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
AnimatedContent(targetState = mode, label = "picker mode") { pickerMode ->
|
||||
when (pickerMode) {
|
||||
ColourPickerMode.Sliders -> {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.colour_picker_hue),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
Text(
|
||||
hueComponent.toString(),
|
||||
style = MaterialTheme.typography.labelLarge.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.6f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Slider(
|
||||
value = hueComponent.toFloat(),
|
||||
onValueChange = {
|
||||
selectedColour =
|
||||
(selectedColour and 0x00FFFF) or (it.toInt() shl 16)
|
||||
},
|
||||
valueRange = 0f..255f,
|
||||
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
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
track = {
|
||||
Canvas(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
) {
|
||||
drawRect(
|
||||
Brush.horizontalGradient(hueTrackColours, endX = size.width)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.colour_picker_saturation),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
Text(
|
||||
saturationComponent.toString(),
|
||||
style = MaterialTheme.typography.labelLarge.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.6f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Slider(
|
||||
value = saturationComponent.toFloat(),
|
||||
onValueChange = {
|
||||
selectedColour =
|
||||
(selectedColour and 0xFF00FF) or (it.toInt() shl 8)
|
||||
},
|
||||
valueRange = 0f..255f,
|
||||
colors = SliderDefaults.colors().copy(
|
||||
thumbColor = Color(
|
||||
android.graphics.Color.HSVToColor(
|
||||
floatArrayOf(
|
||||
hueComponent.toFloat(),
|
||||
(selectedColour shr 8 and 0xFF) / 255f,
|
||||
1f
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
track = {
|
||||
Canvas(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
) {
|
||||
drawRect(
|
||||
Brush.horizontalGradient(
|
||||
listOf(
|
||||
Color(
|
||||
android.graphics.Color.HSVToColor(
|
||||
floatArrayOf(
|
||||
hueComponent.toFloat(),
|
||||
0f,
|
||||
1f
|
||||
)
|
||||
)
|
||||
),
|
||||
Color(
|
||||
android.graphics.Color.HSVToColor(
|
||||
floatArrayOf(
|
||||
hueComponent.toFloat(),
|
||||
1f,
|
||||
1f
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
endX = size.width
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.colour_picker_value),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
Text(
|
||||
valueComponent.toString(),
|
||||
style = MaterialTheme.typography.labelLarge.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(
|
||||
alpha = 0.6f
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Slider(
|
||||
value = valueComponent.toFloat(),
|
||||
onValueChange = {
|
||||
selectedColour =
|
||||
(selectedColour and 0xFFFF00) or it.toInt()
|
||||
},
|
||||
valueRange = 0f..255f,
|
||||
colors = SliderDefaults.colors().copy(
|
||||
thumbColor = Color(
|
||||
android.graphics.Color.HSVToColor(
|
||||
floatArrayOf(
|
||||
hueComponent.toFloat(),
|
||||
(selectedColour shr 8 and 0xFF) / 255f,
|
||||
(selectedColour and 0xFF) / 255f
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
track = {
|
||||
Canvas(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
) {
|
||||
drawRect(
|
||||
Brush.horizontalGradient(
|
||||
listOf(
|
||||
Color(
|
||||
android.graphics.Color.HSVToColor(
|
||||
floatArrayOf(
|
||||
hueComponent.toFloat(),
|
||||
(selectedColour shr 8 and 0xFF) / 255f,
|
||||
0f
|
||||
)
|
||||
)
|
||||
),
|
||||
Color(
|
||||
android.graphics.Color.HSVToColor(
|
||||
floatArrayOf(
|
||||
hueComponent.toFloat(),
|
||||
(selectedColour shr 8 and 0xFF) / 255f,
|
||||
1f
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
endX = size.width
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ColourPickerMode.Palette -> {
|
||||
Text("TODO: Palette picker", Modifier.fillMaxWidth())
|
||||
}
|
||||
|
||||
ColourPickerMode.Hex -> {
|
||||
OutlinedTextField(
|
||||
value = pendingHexColourString,
|
||||
onValueChange = {
|
||||
pendingHexColourString = it
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
stringResource(R.string.colour_picker_preview),
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
|
||||
Canvas(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(64.dp)
|
||||
) {
|
||||
drawRect(
|
||||
Color(
|
||||
android.graphics.Color.HSVToColor(
|
||||
floatArrayOf(
|
||||
hueComponent.toFloat(),
|
||||
saturationComponent / 255f,
|
||||
valueComponent / 255f
|
||||
)
|
||||
)
|
||||
),
|
||||
size = size
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SheetEnd()
|
||||
}
|
||||
|
|
@ -453,6 +453,21 @@
|
|||
<string name="media_viewer_share_video">Share video</string>
|
||||
<string name="media_viewer_share_image">Share image</string>
|
||||
|
||||
<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_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_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_cancel">Cancel</string>
|
||||
<string name="colour_picker_apply">Apply</string>
|
||||
|
||||
<string name="file_picker_cannot_attach_file_invalid">This file is invalid and cannot be attached.</string>
|
||||
<string name="file_picker_permission_request_header">We need your permission to access photos and videos</string>
|
||||
<string name="file_picker_permission_request_body">You will be able to attach photos and videos to your messages afterwards.</string>
|
||||
|
|
|
|||
Loading…
Reference in New Issue