fix(amethyst): polish UI Preferences from the design audit

Applies the audit findings on the reworked screen:

- Cards get a 1dp outlineVariant hairline so they separate from the page in
  light mode, where surfaceContainerLow (#F7F7F7) barely differs from the
  background (#FDFDFD). Applies to every SettingsSection, keeping settings
  screens consistent.
- Segmented buttons drop the default selected check icon (the fill already
  signals selection) and use compact labels in the tight 3-4-up rows
  (Wi-Fi, Full/Simple/Fast, System/Sans/Serif/Mono) so labels stop
  ellipsizing to "Unmet…", "Simpl…", "System D…".
- Language becomes a disclosure row (icon + title + current language +
  chevron, whole row opens the existing picker dialog) instead of a lone
  text-field dropdown, matching the app's other "opens a picker" rows.
- Font-size preview uses a gentler 13sp base so the row no longer lurches
  taller for "Huge"; still previews small-to-huge.
- Accent swatches use FlowRow so all six wrap into view instead of scrolling
  off-edge with no affordance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cy9tooutEdFQV5CwMhaP3j
This commit is contained in:
Claude
2026-07-17 02:21:29 +00:00
parent 946303c2e5
commit 5bb0140543
3 changed files with 99 additions and 18 deletions
@@ -25,16 +25,18 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
@@ -50,7 +52,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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
@@ -78,6 +82,7 @@ import com.vitorpamplona.amethyst.model.FontFamilyType
import com.vitorpamplona.amethyst.model.FontSizeType
import com.vitorpamplona.amethyst.model.ThemeType
import com.vitorpamplona.amethyst.model.UiSettingsFlow
import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog
import com.vitorpamplona.amethyst.ui.components.TextSpinner
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
@@ -196,6 +201,9 @@ private fun <T> SegmentedChoiceTile(
selected = option == selected,
onClick = { onSelect(option) },
shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size),
// Drop the default check icon: the fill already signals selection, and the icon
// steals ~24dp that the label needs in a 34-up row.
icon = {},
) {
Text(
text = stringRes(labelRes(option)),
@@ -245,6 +253,33 @@ private fun ThemeTile(sharedPrefs: UiSettingsFlow) {
)
}
// Compact labels for the connectivity segmented rows — "Unmetered WiFi" does not fit a 3-up
// segment, so it collapses to "Wi-Fi"; the others are already short.
@StringRes
private fun ConnectivityType.shortLabelRes(): Int =
when (this) {
ConnectivityType.ALWAYS -> R.string.connectivity_type_always
ConnectivityType.WIFI_ONLY -> R.string.connectivity_type_unmetered_wifi_only_short
ConnectivityType.NEVER -> R.string.connectivity_type_never
}
@StringRes
private fun FontFamilyType.shortLabelRes(): Int =
when (this) {
FontFamilyType.SYSTEM -> R.string.font_family_system_short
FontFamilyType.SANS_SERIF -> R.string.font_family_sans_serif_short
FontFamilyType.SERIF -> R.string.font_family_serif_short
FontFamilyType.MONOSPACE -> R.string.font_family_monospace_short
}
@StringRes
private fun FeatureSetType.shortLabelRes(): Int =
when (this) {
FeatureSetType.COMPLETE -> R.string.ui_feature_set_type_complete_short
FeatureSetType.SIMPLIFIED -> R.string.ui_feature_set_type_simplified_short
FeatureSetType.PERFORMANCE -> R.string.ui_feature_set_type_performance_short
}
@Composable
private fun FontFamilyTile(sharedPrefs: UiSettingsFlow) {
val fontFamily by sharedPrefs.fontFamily.collectAsState()
@@ -253,7 +288,7 @@ private fun FontFamilyTile(sharedPrefs: UiSettingsFlow) {
title = R.string.font_family,
description = R.string.font_family_description,
options = FontFamilyType.entries,
labelRes = { it.resourceId },
labelRes = { it.shortLabelRes() },
selected = fontFamily,
onSelect = { sharedPrefs.fontFamily.tryEmit(it) },
// Draw each option's name in the very typeface it selects.
@@ -272,8 +307,9 @@ private fun FontSizeTile(sharedPrefs: UiSettingsFlow) {
labelRes = { it.resourceId },
selected = fontSize,
onSelect = { sharedPrefs.fontSize.tryEmit(it) },
// Scale each option's label to the size it selects, so Small looks small and Huge huge.
optionTextStyle = { LocalTextStyle.current.copy(fontSize = 15.sp * it.scale) },
// Scale each label to preview its size — Small looks small, Huge looks huge. A small 13sp
// base keeps the spread gentle (~1117sp) so the row stays tidy instead of lurching taller.
optionTextStyle = { LocalTextStyle.current.copy(fontSize = 13.sp * it.scale) },
)
}
@@ -285,7 +321,7 @@ private fun UiModeTile(sharedPrefs: UiSettingsFlow) {
title = R.string.ui_style,
description = R.string.ui_style_description,
options = FeatureSetType.entries,
labelRes = { it.resourceId },
labelRes = { it.shortLabelRes() },
selected = featureSet,
onSelect = { sharedPrefs.featureSet.tryEmit(it) },
)
@@ -299,7 +335,7 @@ private fun ImagePreviewTile(sharedPrefs: UiSettingsFlow) {
title = R.string.automatically_load_images_gifs,
description = R.string.automatically_load_images_gifs_description,
options = ConnectivityType.entries,
labelRes = { it.resourceId },
labelRes = { it.shortLabelRes() },
selected = value,
onSelect = { sharedPrefs.automaticallyShowImages.tryEmit(it) },
)
@@ -313,7 +349,7 @@ private fun VideoPlaybackTile(sharedPrefs: UiSettingsFlow) {
title = R.string.automatically_play_videos,
description = R.string.automatically_play_videos_description,
options = ConnectivityType.entries,
labelRes = { it.resourceId },
labelRes = { it.shortLabelRes() },
selected = value,
onSelect = { sharedPrefs.automaticallyStartPlayback.tryEmit(it) },
)
@@ -327,7 +363,7 @@ private fun UrlPreviewTile(sharedPrefs: UiSettingsFlow) {
title = R.string.automatically_show_url_preview,
description = R.string.automatically_show_url_preview_description,
options = ConnectivityType.entries,
labelRes = { it.resourceId },
labelRes = { it.shortLabelRes() },
selected = value,
onSelect = { sharedPrefs.automaticallyShowUrlPreview.tryEmit(it) },
)
@@ -341,7 +377,7 @@ private fun ProfilePictureTile(sharedPrefs: UiSettingsFlow) {
title = R.string.automatically_show_profile_picture,
description = R.string.automatically_show_profile_picture_description,
options = ConnectivityType.entries,
labelRes = { it.resourceId },
labelRes = { it.shortLabelRes() },
selected = value,
onSelect = { sharedPrefs.automaticallyShowProfilePictures.tryEmit(it) },
)
@@ -367,6 +403,7 @@ private fun ImmersiveScrollingTile(sharedPrefs: UiSettingsFlow) {
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun AccentColorTile(sharedPrefs: UiSettingsFlow) {
val accent by sharedPrefs.accentColor.collectAsState()
@@ -377,9 +414,12 @@ private fun AccentColorTile(sharedPrefs: UiSettingsFlow) {
title = stringRes(R.string.accent_color),
description = stringRes(R.string.accent_color_description),
) {
Row(
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
// FlowRow so every swatch stays visible (wraps to a second line on narrow screens) instead
// of scrolling the last ones off-edge with no affordance.
FlowRow(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
AccentColorType.entries.forEach { option ->
AccentColorSwatch(
@@ -393,6 +433,9 @@ private fun AccentColorTile(sharedPrefs: UiSettingsFlow) {
}
}
// Language can't be a segmented row (50+ locales), so it reads as a disclosure row — icon + title +
// the current language on the right + a chevron — and tapping anywhere opens the picker dialog. That
// matches the app's other "opens a picker" rows instead of leaving a lone text-field dropdown here.
@Composable
private fun LanguageTile(sharedPrefs: UiSettingsFlow) {
val context = LocalContext.current
@@ -402,19 +445,42 @@ private fun LanguageTile(sharedPrefs: UiSettingsFlow) {
val language by sharedPrefs.preferredLanguage.collectAsState()
val languageIndex = getLanguageIndex(languageEntries, language)
val currentLabel = languageList.getOrNull(languageIndex)?.title ?: ""
SettingsBlockTile(
var showPicker by remember { mutableStateOf(false) }
SettingsControlRow(
icon = MaterialSymbols.Language,
title = stringRes(R.string.language),
description = stringRes(R.string.language_description),
onClick = { showPicker = true },
) {
TextSpinner(
label = "",
placeholder = languageList.getOrNull(languageIndex)?.title ?: "",
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = currentLabel,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.widthIn(max = 140.dp),
)
Icon(
symbol = MaterialSymbols.ChevronRight,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 4.dp).size(20.dp),
)
}
}
if (showPicker) {
SpinnerSelectionDialog(
options = languageList,
onSelect = { sharedPrefs.preferredLanguage.tryEmit(languageEntries[languageList[it].title]) },
modifier = Modifier.fillMaxWidth(),
)
onDismiss = { showPicker = false },
) { index ->
showPicker = false
sharedPrefs.preferredLanguage.tryEmit(languageEntries[languageList[index].title])
}
}
}
@@ -21,6 +21,7 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
import androidx.annotation.StringRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -83,6 +84,9 @@ internal fun SettingsSection(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
// A hairline edge so the card separates from the page even when the surface tones are
// close (light mode: surfaceContainerLow #F7F7F7 sits on background #FDFDFD).
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
) {
Column(content = content)
}
+11
View File
@@ -1942,10 +1942,16 @@
<string name="connectivity_type_always">Always</string>
<string name="connectivity_type_unmetered_wifi_only">Unmetered WiFi</string>
<string name="connectivity_type_never">Never</string>
<!-- Compact label for the segmented control, where "Unmetered WiFi" does not fit. -->
<string name="connectivity_type_unmetered_wifi_only_short">Wi-Fi</string>
<string name="ui_feature_set_type_complete">Complete</string>
<string name="ui_feature_set_type_simplified">Simplified</string>
<string name="ui_feature_set_type_performance">Performance</string>
<!-- Compact labels for the segmented control. -->
<string name="ui_feature_set_type_complete_short">Full</string>
<string name="ui_feature_set_type_simplified_short">Simple</string>
<string name="ui_feature_set_type_performance_short">Fast</string>
<string name="gallery_type_classic">Classic</string>
<string name="gallery_type_modern">Modern</string>
@@ -1974,6 +1980,11 @@
<string name="font_family_sans_serif">Sans Serif</string>
<string name="font_family_serif">Serif</string>
<string name="font_family_monospace">Monospace</string>
<!-- Compact labels for the segmented control, rendered in each option's own typeface. -->
<string name="font_family_system_short">System</string>
<string name="font_family_sans_serif_short">Sans</string>
<string name="font_family_serif_short">Serif</string>
<string name="font_family_monospace_short">Mono</string>
<string name="font_size">Font Size</string>
<string name="font_size_description">Scale the text size across the app</string>
<string name="font_size_small">Small</string>