From ece16e8373464c71b7d03114179210a3ae0bb213 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 00:39:45 +0000 Subject: [PATCH 1/6] feat(amethyst): modernize UI Preferences with grouped in-screen options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the flat list of dropdown (TextSpinner) rows on the UI Preferences screen with the card-based settings design already used by the Compose and security settings: contextual SettingsSection cards holding in-screen SingleChoiceSegmentedButtonRow controls and Switch tiles, so every option is visible and one tap away instead of hidden behind a spinner. Group the settings by context: - Appearance: theme, accent color, font, font size - Media & Data: image preview, video playback, autoplay, URL preview, profile pictures - General: language, UI mode, immersive scrolling The two boolean (Always/Never) settings — autoplay videos and immersive scrolling — become proper switches. Language keeps a dropdown (too many locales for segmented buttons) but restyled into the new card. The externally-consumed SettingsRow overloads and language helpers are kept. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Cy9tooutEdFQV5CwMhaP3j --- .../loggedIn/settings/AppSettingsScreen.kt | 591 +++++++++--------- amethyst/src/main/res/values/strings.xml | 3 + 2 files changed, 297 insertions(+), 297 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index f0c9abca96..aed5db3685 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -21,9 +21,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings import android.content.Context +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 @@ -39,6 +41,10 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -58,38 +64,31 @@ import androidx.compose.ui.unit.dp import androidx.core.os.LocaleListCompat import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.model.AccentColorType +import com.vitorpamplona.amethyst.model.BooleanType import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.FeatureSetType 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.model.parseBooleanType -import com.vitorpamplona.amethyst.model.parseConnectivityType -import com.vitorpamplona.amethyst.model.parseFeatureSetType -import com.vitorpamplona.amethyst.model.parseFontFamilyType -import com.vitorpamplona.amethyst.model.parseFontSizeType -import com.vitorpamplona.amethyst.model.parseThemeType import com.vitorpamplona.amethyst.ui.components.TextSpinner import com.vitorpamplona.amethyst.ui.components.TitleExplainer import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.RowColSpacing -import com.vitorpamplona.amethyst.ui.theme.Size10dp -import com.vitorpamplona.amethyst.ui.theme.Size20dp -import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.contentColorOnAccent import com.vitorpamplona.amethyst.ui.theme.isLight import com.vitorpamplona.amethyst.ui.theme.previewColor import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap +import kotlinx.coroutines.flow.MutableStateFlow import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException import java.io.IOException @@ -104,42 +103,303 @@ fun SettingsScreen( TopBarWithBackButton(stringRes(id = R.string.application_preferences), nav) }, ) { - Column(Modifier.padding(it)) { - SettingsScreen(accountViewModel.settings.uiSettingsFlow) - } + SettingsScreen(accountViewModel.settings.uiSettingsFlow, Modifier.padding(it)) } } @Preview(device = "spec:width=2160px,height=2340px,dpi=440") @Composable fun SettingsScreenPreview() { - ThemeComparisonRow { + ThemeComparisonColumn { SettingsScreen(UiSettingsFlow()) } } @Composable -fun SettingsScreen(sharedPrefs: UiSettingsFlow) { +fun SettingsScreen( + sharedPrefs: UiSettingsFlow, + modifier: Modifier = Modifier, +) { Column( - Modifier - .fillMaxSize() - .padding(top = Size10dp, start = Size20dp, end = Size20dp) - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = RowColSpacing, + modifier = + modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), ) { - ShowLanguageChoice(sharedPrefs) - ShowThemeChoice(sharedPrefs) - ShowAccentColorChoice(sharedPrefs) - ShowFontFamilyChoice(sharedPrefs) - ShowFontSizeChoice(sharedPrefs) - ShowImagePreviewChoice(sharedPrefs) - ShowVideoPlaybackChoice(sharedPrefs) - AutoplayVideosChoice(sharedPrefs) - ShowUrlPreviewChoice(sharedPrefs) - ShowProfilePictureChoice(sharedPrefs) - ImmersiveScrollingChoice(sharedPrefs) - FeatureSetChoice(sharedPrefs) + SettingsSection(R.string.settings_section_appearance) { + ThemeTile(sharedPrefs) + SettingsDivider() + AccentColorTile(sharedPrefs) + SettingsDivider() + FontFamilyTile(sharedPrefs) + SettingsDivider() + FontSizeTile(sharedPrefs) + } + + SettingsSection(R.string.settings_section_media) { + ImagePreviewTile(sharedPrefs) + SettingsDivider() + VideoPlaybackTile(sharedPrefs) + SettingsDivider() + AutoplayVideosTile(sharedPrefs) + SettingsDivider() + UrlPreviewTile(sharedPrefs) + SettingsDivider() + ProfilePictureTile(sharedPrefs) + } + + SettingsSection(R.string.settings_section_general) { + LanguageTile(sharedPrefs) + SettingsDivider() + UiModeTile(sharedPrefs) + SettingsDivider() + ImmersiveScrollingTile(sharedPrefs) + } + } +} + +/** + * A [SettingsBlockTile] whose control is a full-width [SingleChoiceSegmentedButtonRow]. + * This is the in-screen replacement for the old dropdown ([TextSpinner]) rows: every + * option is visible and one tap away. Best for 2–4 mutually-exclusive options. + */ +@Composable +private fun SegmentedChoiceTile( + icon: MaterialSymbol, + @StringRes title: Int, + @StringRes description: Int, + options: List, + labelRes: (T) -> Int, + selected: T, + onSelect: (T) -> Unit, +) { + SettingsBlockTile( + icon = icon, + title = stringRes(title), + description = stringRes(description), + ) { + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + options.forEachIndexed { index, option -> + SegmentedButton( + selected = option == selected, + onClick = { onSelect(option) }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size), + ) { + Text( + text = stringRes(labelRes(option)), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } + } + } +} + +/** A [SettingsControlRow] with a [Switch] backed by a [BooleanType] flow (ALWAYS ⇔ on). */ +@Composable +private fun BooleanSwitchTile( + flow: MutableStateFlow, + icon: MaterialSymbol, + @StringRes title: Int, + @StringRes description: Int, +) { + val value by flow.collectAsState() + val checked = value == BooleanType.ALWAYS + val toggle = { isOn: Boolean -> flow.tryEmit(if (isOn) BooleanType.ALWAYS else BooleanType.NEVER) } + + SettingsControlRow( + icon = icon, + title = stringRes(title), + description = stringRes(description), + onClick = { toggle(!checked) }, + ) { + Switch(checked = checked, onCheckedChange = toggle) + } +} + +@Composable +private fun ThemeTile(sharedPrefs: UiSettingsFlow) { + val theme by sharedPrefs.theme.collectAsState() + SegmentedChoiceTile( + icon = MaterialSymbols.BrightnessMedium, + title = R.string.theme, + description = R.string.theme_description, + options = ThemeType.entries, + labelRes = { it.resourceId }, + selected = theme, + onSelect = { sharedPrefs.theme.tryEmit(it) }, + ) +} + +@Composable +private fun FontFamilyTile(sharedPrefs: UiSettingsFlow) { + val fontFamily by sharedPrefs.fontFamily.collectAsState() + SegmentedChoiceTile( + icon = MaterialSymbols.Article, + title = R.string.font_family, + description = R.string.font_family_description, + options = FontFamilyType.entries, + labelRes = { it.resourceId }, + selected = fontFamily, + onSelect = { sharedPrefs.fontFamily.tryEmit(it) }, + ) +} + +@Composable +private fun FontSizeTile(sharedPrefs: UiSettingsFlow) { + val fontSize by sharedPrefs.fontSize.collectAsState() + SegmentedChoiceTile( + icon = MaterialSymbols.ZoomOutMap, + title = R.string.font_size, + description = R.string.font_size_description, + options = FontSizeType.entries, + labelRes = { it.resourceId }, + selected = fontSize, + onSelect = { sharedPrefs.fontSize.tryEmit(it) }, + ) +} + +@Composable +private fun UiModeTile(sharedPrefs: UiSettingsFlow) { + val featureSet by sharedPrefs.featureSet.collectAsState() + SegmentedChoiceTile( + icon = MaterialSymbols.Tune, + title = R.string.ui_style, + description = R.string.ui_style_description, + options = FeatureSetType.entries, + labelRes = { it.resourceId }, + selected = featureSet, + onSelect = { sharedPrefs.featureSet.tryEmit(it) }, + ) +} + +@Composable +private fun ImagePreviewTile(sharedPrefs: UiSettingsFlow) { + val value by sharedPrefs.automaticallyShowImages.collectAsState() + SegmentedChoiceTile( + icon = MaterialSymbols.Image, + title = R.string.automatically_load_images_gifs, + description = R.string.automatically_load_images_gifs_description, + options = ConnectivityType.entries, + labelRes = { it.resourceId }, + selected = value, + onSelect = { sharedPrefs.automaticallyShowImages.tryEmit(it) }, + ) +} + +@Composable +private fun VideoPlaybackTile(sharedPrefs: UiSettingsFlow) { + val value by sharedPrefs.automaticallyStartPlayback.collectAsState() + SegmentedChoiceTile( + icon = MaterialSymbols.Videocam, + title = R.string.automatically_play_videos, + description = R.string.automatically_play_videos_description, + options = ConnectivityType.entries, + labelRes = { it.resourceId }, + selected = value, + onSelect = { sharedPrefs.automaticallyStartPlayback.tryEmit(it) }, + ) +} + +@Composable +private fun UrlPreviewTile(sharedPrefs: UiSettingsFlow) { + val value by sharedPrefs.automaticallyShowUrlPreview.collectAsState() + SegmentedChoiceTile( + icon = MaterialSymbols.Link, + title = R.string.automatically_show_url_preview, + description = R.string.automatically_show_url_preview_description, + options = ConnectivityType.entries, + labelRes = { it.resourceId }, + selected = value, + onSelect = { sharedPrefs.automaticallyShowUrlPreview.tryEmit(it) }, + ) +} + +@Composable +private fun ProfilePictureTile(sharedPrefs: UiSettingsFlow) { + val value by sharedPrefs.automaticallyShowProfilePictures.collectAsState() + SegmentedChoiceTile( + icon = MaterialSymbols.AccountCircle, + title = R.string.automatically_show_profile_picture, + description = R.string.automatically_show_profile_picture_description, + options = ConnectivityType.entries, + labelRes = { it.resourceId }, + selected = value, + onSelect = { sharedPrefs.automaticallyShowProfilePictures.tryEmit(it) }, + ) +} + +@Composable +private fun AutoplayVideosTile(sharedPrefs: UiSettingsFlow) { + BooleanSwitchTile( + flow = sharedPrefs.automaticallyPlayVideos, + icon = MaterialSymbols.PlayCircle, + title = R.string.autoplay_videos, + description = R.string.autoplay_videos_description, + ) +} + +@Composable +private fun ImmersiveScrollingTile(sharedPrefs: UiSettingsFlow) { + BooleanSwitchTile( + flow = sharedPrefs.automaticallyHideNavigationBars, + icon = MaterialSymbols.Fullscreen, + title = R.string.automatically_hide_nav_bars, + description = R.string.automatically_hide_nav_bars_description, + ) +} + +@Composable +private fun AccentColorTile(sharedPrefs: UiSettingsFlow) { + val accent by sharedPrefs.accentColor.collectAsState() + val dark = !MaterialTheme.colorScheme.isLight + + SettingsBlockTile( + icon = MaterialSymbols.Circle, + title = stringRes(R.string.accent_color), + description = stringRes(R.string.accent_color_description), + ) { + Row( + modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + AccentColorType.entries.forEach { option -> + AccentColorSwatch( + color = option.previewColor(dark), + label = stringRes(option.resourceId), + selected = option == accent, + onClick = { sharedPrefs.accentColor.tryEmit(option) }, + ) + } + } + } +} + +@Composable +private fun LanguageTile(sharedPrefs: UiSettingsFlow) { + val context = LocalContext.current + + val languageEntries = remember { context.getLangPreferenceDropdownEntries() } + val languageList = remember { languageEntries.keys.map { TitleExplainer(it) }.toImmutableList() } + + val language by sharedPrefs.preferredLanguage.collectAsState() + val languageIndex = getLanguageIndex(languageEntries, language) + + SettingsBlockTile( + icon = MaterialSymbols.Language, + title = stringRes(R.string.language), + description = stringRes(R.string.language_description), + ) { + TextSpinner( + label = "", + placeholder = languageList.getOrNull(languageIndex)?.title ?: "", + options = languageList, + onSelect = { sharedPrefs.preferredLanguage.tryEmit(languageEntries[languageList[it].title]) }, + modifier = Modifier.fillMaxWidth(), + ) } } @@ -195,80 +455,6 @@ fun getLanguageIndex( return languageIndex } -@Composable -fun ShowLanguageChoice(sharedPrefs: UiSettingsFlow) { - val context = LocalContext.current - - val languageEntries = remember { context.getLangPreferenceDropdownEntries() } - val languageList = remember { languageEntries.keys.map { TitleExplainer(it) }.toImmutableList() } - - val language by sharedPrefs.preferredLanguage.collectAsState() - - val languageIndex = getLanguageIndex(languageEntries, language) - - SettingsRow( - R.string.language, - R.string.language_description, - languageList, - languageIndex, - ) { - sharedPrefs.preferredLanguage.tryEmit(languageEntries[languageList[it].title]) - } -} - -@Composable -fun ShowThemeChoice(sharedPrefs: UiSettingsFlow) { - val themeOptions = - persistentListOf( - TitleExplainer(stringRes(ThemeType.SYSTEM.resourceId)), - TitleExplainer(stringRes(ThemeType.LIGHT.resourceId)), - TitleExplainer(stringRes(ThemeType.DARK.resourceId)), - ) - - val themeIndex by sharedPrefs.theme.collectAsState() - - SettingsRow( - R.string.theme, - R.string.theme_description, - themeOptions, - themeIndex.screenCode, - ) { - sharedPrefs.theme.tryEmit(parseThemeType(it)) - } -} - -@Composable -fun ShowAccentColorChoice(sharedPrefs: UiSettingsFlow) { - val accent by sharedPrefs.accentColor.collectAsState() - val dark = !MaterialTheme.colorScheme.isLight - - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = stringRes(R.string.accent_color), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - Text( - text = stringRes(R.string.accent_color_description), - style = MaterialTheme.typography.bodySmall, - color = Color.Gray, - ) - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - AccentColorType.entries.forEach { option -> - AccentColorSwatch( - color = option.previewColor(dark), - label = stringRes(option.resourceId), - selected = option == accent, - onClick = { sharedPrefs.accentColor.tryEmit(option) }, - ) - } - } - } -} - @Composable private fun AccentColorSwatch( color: Color, @@ -303,195 +489,6 @@ private fun AccentColorSwatch( } } -@Composable -fun ShowFontFamilyChoice(sharedPrefs: UiSettingsFlow) { - val fontOptions = - persistentListOf( - TitleExplainer(stringRes(FontFamilyType.SYSTEM.resourceId)), - TitleExplainer(stringRes(FontFamilyType.SANS_SERIF.resourceId)), - TitleExplainer(stringRes(FontFamilyType.SERIF.resourceId)), - TitleExplainer(stringRes(FontFamilyType.MONOSPACE.resourceId)), - ) - - val fontIndex by sharedPrefs.fontFamily.collectAsState() - - SettingsRow( - R.string.font_family, - R.string.font_family_description, - fontOptions, - fontIndex.screenCode, - ) { - sharedPrefs.fontFamily.tryEmit(parseFontFamilyType(it)) - } -} - -@Composable -fun ShowFontSizeChoice(sharedPrefs: UiSettingsFlow) { - val fontSizeOptions = - persistentListOf( - TitleExplainer(stringRes(FontSizeType.SMALL.resourceId)), - TitleExplainer(stringRes(FontSizeType.NORMAL.resourceId)), - TitleExplainer(stringRes(FontSizeType.LARGE.resourceId)), - TitleExplainer(stringRes(FontSizeType.HUGE.resourceId)), - ) - - val fontSizeIndex by sharedPrefs.fontSize.collectAsState() - - SettingsRow( - R.string.font_size, - R.string.font_size_description, - fontSizeOptions, - fontSizeIndex.screenCode, - ) { - sharedPrefs.fontSize.tryEmit(parseFontSizeType(it)) - } -} - -@Composable -fun ShowImagePreviewChoice(sharedPrefs: UiSettingsFlow) { - val connectivityBasedOptions = - persistentListOf( - TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)), - TitleExplainer(stringRes(ConnectivityType.WIFI_ONLY.resourceId)), - TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)), - ) - - val showImagesIndex by sharedPrefs.automaticallyShowImages.collectAsState() - - SettingsRow( - R.string.automatically_load_images_gifs, - R.string.automatically_load_images_gifs_description, - connectivityBasedOptions, - showImagesIndex.screenCode, - ) { - sharedPrefs.automaticallyShowImages.tryEmit(parseConnectivityType(it)) - } -} - -@Composable -fun ShowVideoPlaybackChoice(sharedPrefs: UiSettingsFlow) { - val connectivityBasedOptions = - persistentListOf( - TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)), - TitleExplainer(stringRes(ConnectivityType.WIFI_ONLY.resourceId)), - TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)), - ) - - val videoIndex by sharedPrefs.automaticallyStartPlayback.collectAsState() - - SettingsRow( - R.string.automatically_play_videos, - R.string.automatically_play_videos_description, - connectivityBasedOptions, - videoIndex.screenCode, - ) { - sharedPrefs.automaticallyStartPlayback.tryEmit(parseConnectivityType(it)) - } -} - -@Composable -fun AutoplayVideosChoice(sharedPrefs: UiSettingsFlow) { - val autoplayIndex by sharedPrefs.automaticallyPlayVideos.collectAsState() - - val booleanItems = - persistentListOf( - TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)), - TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)), - ) - - SettingsRow( - R.string.autoplay_videos, - R.string.autoplay_videos_description, - booleanItems, - autoplayIndex.screenCode, - ) { - sharedPrefs.automaticallyPlayVideos.tryEmit(parseBooleanType(it)) - } -} - -@Composable -fun ShowUrlPreviewChoice(sharedPrefs: UiSettingsFlow) { - val connectivityBasedOptions = - persistentListOf( - TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)), - TitleExplainer(stringRes(ConnectivityType.WIFI_ONLY.resourceId)), - TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)), - ) - - val linkIndex by sharedPrefs.automaticallyShowUrlPreview.collectAsState() - - SettingsRow( - R.string.automatically_show_url_preview, - R.string.automatically_show_url_preview_description, - connectivityBasedOptions, - linkIndex.screenCode, - ) { - sharedPrefs.automaticallyShowUrlPreview.tryEmit(parseConnectivityType(it)) - } -} - -@Composable -fun ShowProfilePictureChoice(sharedPrefs: UiSettingsFlow) { - val profilePictureIndex by sharedPrefs.automaticallyShowProfilePictures.collectAsState() - - val connectivityBasedOptions = - persistentListOf( - TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)), - TitleExplainer(stringRes(ConnectivityType.WIFI_ONLY.resourceId)), - TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)), - ) - - SettingsRow( - R.string.automatically_show_profile_picture, - R.string.automatically_show_profile_picture_description, - connectivityBasedOptions, - profilePictureIndex.screenCode, - ) { - sharedPrefs.automaticallyShowProfilePictures.tryEmit(parseConnectivityType(it)) - } -} - -@Composable -fun ImmersiveScrollingChoice(sharedPrefs: UiSettingsFlow) { - val hideNavBarsIndex by sharedPrefs.automaticallyHideNavigationBars.collectAsState() - - val booleanItems = - persistentListOf( - TitleExplainer(stringRes(ConnectivityType.ALWAYS.resourceId)), - TitleExplainer(stringRes(ConnectivityType.NEVER.resourceId)), - ) - - SettingsRow( - R.string.automatically_hide_nav_bars, - R.string.automatically_hide_nav_bars_description, - booleanItems, - hideNavBarsIndex.screenCode, - ) { - sharedPrefs.automaticallyHideNavigationBars.tryEmit(parseBooleanType(it)) - } -} - -@Composable -fun FeatureSetChoice(sharedPrefs: UiSettingsFlow) { - val featureItems = - persistentListOf( - TitleExplainer(stringRes(FeatureSetType.COMPLETE.resourceId)), - TitleExplainer(stringRes(FeatureSetType.SIMPLIFIED.resourceId)), - TitleExplainer(stringRes(FeatureSetType.PERFORMANCE.resourceId)), - ) - - val featureSetIndex by sharedPrefs.featureSet.collectAsState() - - SettingsRow( - R.string.ui_style, - R.string.ui_style_description, - featureItems, - featureSetIndex.screenCode, - ) { - sharedPrefs.featureSet.tryEmit(parseFeatureSetType(it)) - } -} - @Composable fun SettingsRow( name: Int, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 0adf33c233..aae7fdb834 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1954,6 +1954,9 @@ Light Dark Application Preferences + Appearance + Media & Data + General Wallet Connect Language Theme From 946303c2e50df34ec056fd1a249b5187b5b28a44 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 01:23:20 +0000 Subject: [PATCH 2/6] feat(amethyst): make font tiles preview each option live MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The font family and font size segmented rows now render each option's label in the very typeface / at the very scale it selects — Sans Serif in sans, Monospace in mono, Small small and Huge huge — so the control demonstrates the choices instead of only naming them. Adds an optional per-option text-style hook to SegmentedChoiceTile, reusing the existing FontFamilyType.toFontFamily() mapping and FontSizeType.scale. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Cy9tooutEdFQV5CwMhaP3j --- .../screen/loggedIn/settings/AppSettingsScreen.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index aed5db3685..008500e80d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -39,6 +39,7 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SegmentedButton @@ -57,10 +58,13 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.core.os.LocaleListCompat import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -84,6 +88,7 @@ import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.ui.theme.contentColorOnAccent import com.vitorpamplona.amethyst.ui.theme.isLight import com.vitorpamplona.amethyst.ui.theme.previewColor +import com.vitorpamplona.amethyst.ui.theme.toFontFamily import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.toImmutableList @@ -164,6 +169,10 @@ fun SettingsScreen( * A [SettingsBlockTile] whose control is a full-width [SingleChoiceSegmentedButtonRow]. * This is the in-screen replacement for the old dropdown ([TextSpinner]) rows: every * option is visible and one tap away. Best for 2–4 mutually-exclusive options. + * + * [optionTextStyle] lets each option render its own label preview — e.g. the font tiles + * draw each label in the very typeface / size it selects, so the row demonstrates the + * choices instead of only naming them. */ @Composable private fun SegmentedChoiceTile( @@ -174,6 +183,7 @@ private fun SegmentedChoiceTile( labelRes: (T) -> Int, selected: T, onSelect: (T) -> Unit, + optionTextStyle: (@Composable (T) -> TextStyle)? = null, ) { SettingsBlockTile( icon = icon, @@ -191,6 +201,7 @@ private fun SegmentedChoiceTile( text = stringRes(labelRes(option)), maxLines = 1, overflow = TextOverflow.Ellipsis, + style = optionTextStyle?.invoke(option) ?: LocalTextStyle.current, ) } } @@ -245,6 +256,8 @@ private fun FontFamilyTile(sharedPrefs: UiSettingsFlow) { labelRes = { it.resourceId }, selected = fontFamily, onSelect = { sharedPrefs.fontFamily.tryEmit(it) }, + // Draw each option's name in the very typeface it selects. + optionTextStyle = { LocalTextStyle.current.copy(fontFamily = it.toFontFamily() ?: FontFamily.Default) }, ) } @@ -259,6 +272,8 @@ 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) }, ) } From 5bb0140543c2028da52caf91b3e41df8e20da568 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 01:51:53 +0000 Subject: [PATCH 3/6] fix(amethyst): polish UI Preferences from the design audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Cy9tooutEdFQV5CwMhaP3j --- .../loggedIn/settings/AppSettingsScreen.kt | 102 ++++++++++++++---- .../loggedIn/settings/SettingsSectionCard.kt | 4 + amethyst/src/main/res/values/strings.xml | 11 ++ 3 files changed, 99 insertions(+), 18 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index 008500e80d..7c84705ba7 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -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 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]) + } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsSectionCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsSectionCard.kt index a188839456..a2cfe2b89b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsSectionCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsSectionCard.kt @@ -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) } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index aae7fdb834..0fb43aba2d 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1942,10 +1942,16 @@ Always Unmetered WiFi Never + + Wi-Fi Complete Simplified Performance + + Full + Simple + Fast Classic Modern @@ -1974,6 +1980,11 @@ Sans Serif Serif Monospace + + System + Sans + Serif + Mono Font Size Scale the text size across the app Small From 010684c6aeabf60fd8da5475691c0513ec40e19a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 02:14:28 +0000 Subject: [PATCH 4/6] chore(amethyst): full-screen @Preview for UI Preferences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrade the preview to render the real screen — top bar plus the redesigned content — side by side in dark and light (ThemeComparisonRow), so both themes and the card-hairline contrast can be checked at a glance instead of the content-only, chrome-less preview. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Cy9tooutEdFQV5CwMhaP3j --- .../loggedIn/settings/AppSettingsScreen.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index 7c84705ba7..d4b27a8e48 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -85,11 +85,12 @@ 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.EmptyNav import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.stringRes -import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn +import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow import com.vitorpamplona.amethyst.ui.theme.contentColorOnAccent import com.vitorpamplona.amethyst.ui.theme.isLight import com.vitorpamplona.amethyst.ui.theme.previewColor @@ -117,11 +118,19 @@ fun SettingsScreen( } } -@Preview(device = "spec:width=2160px,height=2340px,dpi=440") +// Full-screen preview: the real top bar + the redesigned content, shown side by side in dark and +// light so both grounds (and the card-hairline contrast) can be eyeballed at once. +@Preview(name = "UI Preferences", device = "spec:width=2160px,height=2340px,dpi=440") @Composable fun SettingsScreenPreview() { - ThemeComparisonColumn { - SettingsScreen(UiSettingsFlow()) + ThemeComparisonRow { + Scaffold( + topBar = { + TopBarWithBackButton(stringRes(id = R.string.application_preferences), EmptyNav()) + }, + ) { padding -> + SettingsScreen(UiSettingsFlow(), Modifier.padding(padding)) + } } } From ef6c9bd5c4281217cdfac3b143b3cb860244ad9a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 02:42:55 +0000 Subject: [PATCH 5/6] fix(amethyst): compile errors in UI Preferences tiles Caught by the first successful build after the Gradle distribution became seedable in the web sandbox: - BooleanSwitchTile's toggle inferred as (Boolean) -> Boolean because MutableStateFlow.tryEmit returns Boolean; annotate it (Boolean) -> Unit so it fits Switch.onCheckedChange and SettingsControlRow.onClick. - Font tile referenced MaterialSymbols.Article, which lives under the nested AutoMirrored object; use the top-level MaterialSymbols.Description glyph. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Cy9tooutEdFQV5CwMhaP3j --- .../ui/screen/loggedIn/settings/AppSettingsScreen.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt index d4b27a8e48..2b1176b603 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/AppSettingsScreen.kt @@ -236,7 +236,9 @@ private fun BooleanSwitchTile( ) { val value by flow.collectAsState() val checked = value == BooleanType.ALWAYS - val toggle = { isOn: Boolean -> flow.tryEmit(if (isOn) BooleanType.ALWAYS else BooleanType.NEVER) } + // Explicit Unit return: tryEmit returns Boolean, so the lambda would otherwise infer as + // (Boolean) -> Boolean and not fit Switch.onCheckedChange / SettingsControlRow.onClick. + val toggle: (Boolean) -> Unit = { isOn -> flow.tryEmit(if (isOn) BooleanType.ALWAYS else BooleanType.NEVER) } SettingsControlRow( icon = icon, @@ -293,7 +295,7 @@ private fun FeatureSetType.shortLabelRes(): Int = private fun FontFamilyTile(sharedPrefs: UiSettingsFlow) { val fontFamily by sharedPrefs.fontFamily.collectAsState() SegmentedChoiceTile( - icon = MaterialSymbols.Article, + icon = MaterialSymbols.Description, title = R.string.font_family, description = R.string.font_family_description, options = FontFamilyType.entries, From 069376f2aca99e0dfa25ada143c41b31cddbff12 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 03:46:40 +0000 Subject: [PATCH 6/6] fix(amethyst): separate settings cards by tone, not a hard border MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 1dp outlineVariant outline around every SettingsSection card read boxy. Replace it with a surface-tone step: fill the card with surfaceContainer instead of surfaceContainerLow. The neutral surface ramp puts the page at background #FDFDFD, where …Low (#F7F7F7) was nearly invisible; …Container (#F2F2F2) reads as a soft card in light and #252525 stands clear of black in dark — the Material way to separate a card, without an outline. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Cy9tooutEdFQV5CwMhaP3j --- .../ui/screen/loggedIn/settings/SettingsSectionCard.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsSectionCard.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsSectionCard.kt index a2cfe2b89b..f59bd37618 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsSectionCard.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/settings/SettingsSectionCard.kt @@ -21,7 +21,6 @@ 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 @@ -81,12 +80,13 @@ internal fun SettingsSection( shape = RoundedCornerShape(20.dp), colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow, + // surfaceContainer (one step up from …Low) so the card separates from the page by + // tone rather than a hard outline. The neutral surface ramp puts the page at + // background #FDFDFD, where …Low #F7F7F7 was nearly invisible; #F2F2F2 reads as a + // soft card in light and #252525 stands clear of black in dark. + containerColor = MaterialTheme.colorScheme.surfaceContainer, ), 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) }