feat: add corner radius option for avatars

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-06-16 01:50:49 +02:00
parent 70c906f232
commit 13109c61c2
10 changed files with 282 additions and 3 deletions

View File

@ -25,4 +25,9 @@ data class AndroidSpecificSettings(
* Can be one of `{ None, SwipeFromEnd, DoubleTap }` * Can be one of `{ None, SwipeFromEnd, DoubleTap }`
*/ */
var messageReplyStyle: String? = null, var messageReplyStyle: String? = null,
/**
* Avatar radius.
* Must be integer in range 0..50 inclusive.
*/
var avatarRadius: Int? = null
) )

View File

@ -1,6 +1,7 @@
package chat.revolt.api.settings package chat.revolt.api.settings
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import chat.revolt.ui.theme.Theme import chat.revolt.ui.theme.Theme
@ -15,16 +16,19 @@ enum class MessageReplyStyle {
object GlobalState { object GlobalState {
var theme by mutableStateOf(getDefaultTheme()) var theme by mutableStateOf(getDefaultTheme())
var messageReplyStyle by mutableStateOf(MessageReplyStyle.SwipeFromEnd) var messageReplyStyle by mutableStateOf(MessageReplyStyle.SwipeFromEnd)
var avatarRadius by mutableIntStateOf(50)
fun hydrateWithSettings(settings: SyncedSettings) { fun hydrateWithSettings(settings: SyncedSettings) {
this.theme = settings.android.theme?.let { Theme.valueOf(it) } ?: getDefaultTheme() this.theme = settings.android.theme?.let { Theme.valueOf(it) } ?: getDefaultTheme()
this.messageReplyStyle = this.messageReplyStyle =
settings.android.messageReplyStyle?.let { MessageReplyStyle.valueOf(it) } settings.android.messageReplyStyle?.let { MessageReplyStyle.valueOf(it) }
?: MessageReplyStyle.SwipeFromEnd ?: MessageReplyStyle.SwipeFromEnd
this.avatarRadius = settings.android.avatarRadius ?: 50
} }
fun reset() { fun reset() {
theme = getDefaultTheme() theme = getDefaultTheme()
messageReplyStyle = MessageReplyStyle.SwipeFromEnd messageReplyStyle = MessageReplyStyle.SwipeFromEnd
avatarRadius = 50
} }
} }

View File

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -24,6 +25,7 @@ import chat.revolt.R
import chat.revolt.api.REVOLT_BASE import chat.revolt.api.REVOLT_BASE
import chat.revolt.api.REVOLT_FILES import chat.revolt.api.REVOLT_FILES
import chat.revolt.api.schemas.AutumnResource import chat.revolt.api.schemas.AutumnResource
import chat.revolt.api.settings.GlobalState
enum class Presence { enum class Presence {
Online, Online,
@ -101,7 +103,7 @@ fun UserAvatar(
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
description = stringResource(id = R.string.avatar_alt, username), description = stringResource(id = R.string.avatar_alt, username),
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(RoundedCornerShape(GlobalState.avatarRadius))
.size(size) .size(size)
.then( .then(
if (onLongClick != null || onClick != null) { if (onLongClick != null || onClick != null) {
@ -120,7 +122,7 @@ fun UserAvatar(
url = "$REVOLT_BASE/users/$userId/default_avatar", url = "$REVOLT_BASE/users/$userId/default_avatar",
description = stringResource(id = R.string.avatar_alt, username), description = stringResource(id = R.string.avatar_alt, username),
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(RoundedCornerShape(GlobalState.avatarRadius))
.size(size) .size(size)
.then( .then(
if (onLongClick != null || onClick != null) { if (onLongClick != null || onClick != null) {

View File

@ -0,0 +1,191 @@
package chat.revolt.components.screens.settings.appearance
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.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import chat.revolt.R
enum class CornerRadiusPreset(val percentage: Int) {
SHARP(0),
ROUNDED(15),
CIRCULAR(50),
}
@Composable
fun CornerRadiusPicker(percentage: Int, onUpdate: (Int) -> Unit, modifier: Modifier = Modifier) {
var showOtherModal by remember { mutableStateOf(false) }
if (showOtherModal) {
var sliderPosition by remember { mutableStateOf(percentage.toFloat()) }
AlertDialog(
onDismissRequest = { showOtherModal = false },
title = {
Text(
text = stringResource(R.string.corner_radius_picker_choose_radius),
)
},
text = {
Column {
Spacer(modifier = Modifier.size(16.dp))
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(sliderPosition.toInt()))
.background(MaterialTheme.colorScheme.primary)
.size(64.dp),
)
Text(
sliderPosition.toInt().toString(),
style = MaterialTheme.typography.labelLarge.copy(
fontFeatureSettings = "tnum"
),
)
Slider(
value = sliderPosition,
onValueChange = { sliderPosition = it },
valueRange = 0f..50f,
steps = 51
)
}
}
},
confirmButton = {
Button(
onClick = {
showOtherModal = false
onUpdate(sliderPosition.toInt())
}
) {
Text(stringResource(R.string.corner_radius_picker_choose_radius_yes))
}
},
dismissButton = {
TextButton(
onClick = {
showOtherModal = false
}
) {
Text(stringResource(R.string.corner_radius_picker_choose_radius_cancel))
}
}
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = modifier.clip(MaterialTheme.shapes.medium)
) {
CornerRadiusPickerElement(
selected = percentage == CornerRadiusPreset.SHARP.percentage,
onSelect = {
onUpdate(CornerRadiusPreset.SHARP.percentage)
},
icon = painterResource(R.drawable.ux_corner_sharp),
label = stringResource(R.string.corner_radius_picker_sharp),
)
CornerRadiusPickerElement(
selected = percentage == CornerRadiusPreset.ROUNDED.percentage,
onSelect = {
onUpdate(CornerRadiusPreset.ROUNDED.percentage)
},
icon = painterResource(R.drawable.ux_corner_rounded),
label = stringResource(R.string.corner_radius_picker_rounded),
)
CornerRadiusPickerElement(
selected = percentage == CornerRadiusPreset.CIRCULAR.percentage,
onSelect = {
onUpdate(CornerRadiusPreset.CIRCULAR.percentage)
},
icon = painterResource(R.drawable.ux_corner_circular),
label = stringResource(R.string.corner_radius_picker_circular),
)
CornerRadiusPickerElement(
selected = percentage !in CornerRadiusPreset.entries.map { it.percentage },
onSelect = {
showOtherModal = true
},
icon = painterResource(R.drawable.ux_corner_other),
label = if (percentage !in CornerRadiusPreset.entries.map { it.percentage }) {
percentage.toString()
} else stringResource(R.string.corner_radius_picker_other),
highlightLabel = percentage !in CornerRadiusPreset.entries.map { it.percentage },
)
}
}
@Composable
fun RowScope.CornerRadiusPickerElement(
selected: Boolean,
onSelect: () -> Unit,
icon: Painter,
label: String,
modifier: Modifier = Modifier,
highlightLabel: Boolean = false,
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.clip(MaterialTheme.shapes.medium)
.clickable { onSelect() }
.weight(1f)
.background(
if (selected)
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
else
Color.Transparent
)
.padding(vertical = 8.dp)
) {
Icon(
painter = icon,
contentDescription = null,
tint = if (selected)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f),
)
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = if (highlightLabel)
MaterialTheme.colorScheme.primary
else
MaterialTheme.colorScheme.onSurface,
)
}
}

View File

@ -75,6 +75,7 @@ import chat.revolt.api.settings.GlobalState
import chat.revolt.api.settings.SyncedSettings import chat.revolt.api.settings.SyncedSettings
import chat.revolt.components.generic.ListHeader import chat.revolt.components.generic.ListHeader
import chat.revolt.components.screens.settings.appearance.ColourChip import chat.revolt.components.screens.settings.appearance.ColourChip
import chat.revolt.components.screens.settings.appearance.CornerRadiusPicker
import chat.revolt.ui.theme.ClearRippleTheme import chat.revolt.ui.theme.ClearRippleTheme
import chat.revolt.ui.theme.OverridableColourScheme import chat.revolt.ui.theme.OverridableColourScheme
import chat.revolt.ui.theme.Theme import chat.revolt.ui.theme.Theme
@ -111,6 +112,13 @@ class AppearanceSettingsScreenViewModel @Inject constructor(
} }
} }
fun saveNewAvatarRadius(radius: Int) {
GlobalState.avatarRadius = radius
viewModelScope.launch {
SyncedSettings.updateAndroid(SyncedSettings.android.copy(avatarRadius = radius))
}
}
fun updateColourOverrides(fieldName: String, value: Int?) { fun updateColourOverrides(fieldName: String, value: Int?) {
viewModelScope.launch { viewModelScope.launch {
val overrides = SyncedSettings.android.copy().colourOverrides val overrides = SyncedSettings.android.copy().colourOverrides
@ -389,6 +397,25 @@ fun AppearanceSettingsScreen(
} }
} }
ListHeader {
Text(stringResource(R.string.settings_appearance_avatar_shape))
}
Text(
stringResource(R.string.settings_appearance_avatar_shape_description),
modifier = Modifier
.padding(vertical = 8.dp, horizontal = 16.dp),
)
Box(Modifier.padding(horizontal = 16.dp)) {
CornerRadiusPicker(
percentage = GlobalState.avatarRadius,
onUpdate = {
viewModel.saveNewAvatarRadius(it)
}
)
}
Spacer(modifier = Modifier.height(20.dp)) Spacer(modifier = Modifier.height(20.dp))
Row( Row(

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="1000"
android:viewportHeight="1000">
<path
android:pathData="M763,1000C763,998.3 763,996.7 763,995V772C763,724.6 756.7,678.6 744.9,634.9C854.4,592.2 932,485.6 932,361C932,198.6 800.4,67 638,67C510.5,67 402,148.1 361.2,261.5C321.7,252 280.4,247 238,247H0V297H238C276,297 313,301.5 348.4,309.9C345.5,326.5 344,343.6 344,361C344,523.4 475.6,655 638,655C658.2,655 677.9,653 696.9,649.1C707.4,688.3 713,729.5 713,772V995C713,996.7 713,998.3 713,1000H763ZM403,361C403,358.4 403,355.8 403.1,353.3C515,397.4 605,485.2 652.1,595.6C647.4,595.9 642.7,596 638,596C508.2,596 403,490.8 403,361ZM426.6,258.2C464.7,179.9 545.1,126 638,126C767.8,126 873,231.2 873,361C873,450.6 822.8,528.6 749,568.2C691.3,423.7 574.2,309.5 427.9,255.7C427.5,256.5 427,257.4 426.6,258.2Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="1000"
android:viewportHeight="1000">
<path
android:pathData="M740.4,121L697,215.6L602.7,258.6L697,302L740.4,396.3L783.4,302L878,258.6L783.4,215.6M396.3,224.2L310.3,413.5L121,499.5L310.3,585.5L396.3,774.8L482.3,585.5L671.5,499.5L482.3,413.5M740.4,602.7L697,697L602.7,740.4L697,783.4L740.4,878L783.4,783.4L878,740.4L783.4,697"
android:fillColor="#ffffff"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="1000"
android:viewportHeight="1000">
<path
android:pathData="M638,655C475.6,655 344,523.4 344,361C344,339 346.4,317.6 351,297H0V247H366.9C411.4,141.2 516.1,67 638,67C800.4,67 932,198.6 932,361C932,478.7 862.9,580.2 763,627.2V1000H713V645.3C689.1,651.6 663.9,655 638,655ZM403,361C403,338.8 406.1,317.3 411.8,297H411.8C409.5,305.2 407.6,313.5 406.2,322H581C640.1,322 688,369.9 688,429V590.7C671.9,594.2 655.2,596 638,596C508.2,596 403,490.8 403,361ZM432.5,247C437.3,238.3 442.6,230 448.5,222H581C695.3,222 788,314.7 788,429V541.9C839.9,498.8 873,433.8 873,361C873,231.2 767.8,126 638,126C549.6,126 472.6,174.8 432.5,247H432.5Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="1000"
android:viewportHeight="1000">
<path
android:pathData="M932,361C932,478.7 862.9,580.2 763,627.2V1000H713V645.3C689.1,651.6 663.9,655 638,655C475.6,655 344,523.4 344,361C344,339 346.4,317.6 351,297H0V247H366.9C411.4,141.2 516.1,67 638,67C800.4,67 932,198.6 932,361ZM406.2,322C407.6,313.5 409.5,305.2 411.8,297H411.8C406.1,317.3 403,338.8 403,361C403,490.8 508.2,596 638,596C655.2,596 671.9,594.2 688,590.7V322H406.2ZM448.5,222C442.6,230 437.3,238.3 432.5,247H432.5C472.6,174.8 549.6,126 638,126C767.8,126 873,231.2 873,361C873,433.8 839.9,498.8 788,541.9V222H448.5Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View File

@ -466,6 +466,14 @@
<string name="inline_media_picker_no_media_placeholder">Pick media…</string> <string name="inline_media_picker_no_media_placeholder">Pick media…</string>
<string name="inline_media_picker_remove">Remove</string> <string name="inline_media_picker_remove">Remove</string>
<string name="corner_radius_picker_sharp">Sharp</string>
<string name="corner_radius_picker_rounded">Rounded</string>
<string name="corner_radius_picker_circular">Circular</string>
<string name="corner_radius_picker_other">Other</string>
<string name="corner_radius_picker_choose_radius">Choose radius</string>
<string name="corner_radius_picker_choose_radius_yes">Use</string>
<string name="corner_radius_picker_choose_radius_cancel">Cancel</string>
<string name="emoji_picker_close_skin_tone_menu">Close skin tone menu</string> <string name="emoji_picker_close_skin_tone_menu">Close skin tone menu</string>
<string name="emoji_picker_skin_tone_none">No skin tone</string> <string name="emoji_picker_skin_tone_none">No skin tone</string>
<string name="emoji_picker_skin_tone_fitzpatrick_1_2">Light skin tone</string> <string name="emoji_picker_skin_tone_fitzpatrick_1_2">Light skin tone</string>
@ -516,6 +524,9 @@
<string name="settings_appearance_theme_m3dynamic_unsupported">Material You (unsupported)</string> <string name="settings_appearance_theme_m3dynamic_unsupported">Material You (unsupported)</string>
<string name="settings_appearance_theme_m3dynamic_unsupported_toast">Material You is not supported on this device.</string> <string name="settings_appearance_theme_m3dynamic_unsupported_toast">Material You is not supported on this device.</string>
<string name="settings_appearance_avatar_shape">Profile Picture Shape</string>
<string name="settings_appearance_avatar_shape_description">Choose the rounding grade for profile pictures, including in chat and profiles. This applies to all users.</string>
<string name="settings_appearance_colour_overrides">Colour overrides</string> <string name="settings_appearance_colour_overrides">Colour overrides</string>
<string name="settings_appearance_colour_overrides_primary">Primary</string> <string name="settings_appearance_colour_overrides_primary">Primary</string>
<string name="settings_appearance_colour_overrides_on_primary">On Primary</string> <string name="settings_appearance_colour_overrides_on_primary">On Primary</string>
@ -597,5 +608,5 @@
<string name="share_target_invalid_intent">This is not a valid share intent.</string> <string name="share_target_invalid_intent">This is not a valid share intent.</string>
<string name="share_target_attachment_too_large">This attachment is too large for Revolt (max. $1$s).</string> <string name="share_target_attachment_too_large">This attachment is too large for Revolt (max. $1$s).</string>
<string name="share_target_search_channels">Search channels</string> <string name="share_target_search_channels">Search channels</string>
<string name="share_target_select_channel">Please select a channel to share to.</string> <string name="share_target_select_channel">m select a channel to share to.</string>
</resources> </resources>