mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 16:57:39 +00:00
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:
+84
-18
@@ -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 3–4-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 (~11–17sp) 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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user