mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
Merge pull request #3606 from vitorpamplona/claude/modernize-ui-preferences-56auhn
Redesign settings screen with segmented controls and sections
This commit is contained in:
+388
-299
@@ -21,29 +21,40 @@
|
||||
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.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
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -51,45 +62,44 @@ 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
|
||||
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.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.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.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.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 +114,384 @@ 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))
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
ThemeComparisonRow {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopBarWithBackButton(stringRes(id = R.string.application_preferences), EmptyNav())
|
||||
},
|
||||
) { padding ->
|
||||
SettingsScreen(UiSettingsFlow(), Modifier.padding(padding))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(device = "spec:width=2160px,height=2340px,dpi=440")
|
||||
@Composable
|
||||
fun SettingsScreenPreview() {
|
||||
ThemeComparisonRow {
|
||||
SettingsScreen(UiSettingsFlow())
|
||||
fun SettingsScreen(
|
||||
sharedPrefs: UiSettingsFlow,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
) {
|
||||
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.
|
||||
*
|
||||
* [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 <T> SegmentedChoiceTile(
|
||||
icon: MaterialSymbol,
|
||||
@StringRes title: Int,
|
||||
@StringRes description: Int,
|
||||
options: List<T>,
|
||||
labelRes: (T) -> Int,
|
||||
selected: T,
|
||||
onSelect: (T) -> Unit,
|
||||
optionTextStyle: (@Composable (T) -> TextStyle)? = null,
|
||||
) {
|
||||
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),
|
||||
// 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)),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = optionTextStyle?.invoke(option) ?: LocalTextStyle.current,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A [SettingsControlRow] with a [Switch] backed by a [BooleanType] flow (ALWAYS ⇔ on). */
|
||||
@Composable
|
||||
private fun BooleanSwitchTile(
|
||||
flow: MutableStateFlow<BooleanType>,
|
||||
icon: MaterialSymbol,
|
||||
@StringRes title: Int,
|
||||
@StringRes description: Int,
|
||||
) {
|
||||
val value by flow.collectAsState()
|
||||
val checked = value == BooleanType.ALWAYS
|
||||
// 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,
|
||||
title = stringRes(title),
|
||||
description = stringRes(description),
|
||||
onClick = { toggle(!checked) },
|
||||
) {
|
||||
Switch(checked = checked, onCheckedChange = toggle)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(sharedPrefs: UiSettingsFlow) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = Size10dp, start = Size20dp, end = Size20dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = RowColSpacing,
|
||||
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) },
|
||||
)
|
||||
}
|
||||
|
||||
// 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()
|
||||
SegmentedChoiceTile(
|
||||
icon = MaterialSymbols.Description,
|
||||
title = R.string.font_family,
|
||||
description = R.string.font_family_description,
|
||||
options = FontFamilyType.entries,
|
||||
labelRes = { it.shortLabelRes() },
|
||||
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) },
|
||||
)
|
||||
}
|
||||
|
||||
@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) },
|
||||
// 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) },
|
||||
)
|
||||
}
|
||||
|
||||
@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.shortLabelRes() },
|
||||
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.shortLabelRes() },
|
||||
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.shortLabelRes() },
|
||||
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.shortLabelRes() },
|
||||
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.shortLabelRes() },
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@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),
|
||||
) {
|
||||
ShowLanguageChoice(sharedPrefs)
|
||||
ShowThemeChoice(sharedPrefs)
|
||||
ShowAccentColorChoice(sharedPrefs)
|
||||
ShowFontFamilyChoice(sharedPrefs)
|
||||
ShowFontSizeChoice(sharedPrefs)
|
||||
ShowImagePreviewChoice(sharedPrefs)
|
||||
ShowVideoPlaybackChoice(sharedPrefs)
|
||||
AutoplayVideosChoice(sharedPrefs)
|
||||
ShowUrlPreviewChoice(sharedPrefs)
|
||||
ShowProfilePictureChoice(sharedPrefs)
|
||||
ImmersiveScrollingChoice(sharedPrefs)
|
||||
FeatureSetChoice(sharedPrefs)
|
||||
// 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(
|
||||
color = option.previewColor(dark),
|
||||
label = stringRes(option.resourceId),
|
||||
selected = option == accent,
|
||||
onClick = { sharedPrefs.accentColor.tryEmit(option) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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)
|
||||
val currentLabel = languageList.getOrNull(languageIndex)?.title ?: ""
|
||||
|
||||
var showPicker by remember { mutableStateOf(false) }
|
||||
|
||||
SettingsControlRow(
|
||||
icon = MaterialSymbols.Language,
|
||||
title = stringRes(R.string.language),
|
||||
description = stringRes(R.string.language_description),
|
||||
onClick = { showPicker = true },
|
||||
) {
|
||||
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,
|
||||
onDismiss = { showPicker = false },
|
||||
) { index ->
|
||||
showPicker = false
|
||||
sharedPrefs.preferredLanguage.tryEmit(languageEntries[languageList[index].title])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,80 +547,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 +581,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,
|
||||
|
||||
+5
-1
@@ -80,7 +80,11 @@ 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),
|
||||
) {
|
||||
|
||||
@@ -1943,10 +1943,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>
|
||||
@@ -1955,6 +1961,9 @@
|
||||
<string name="light">Light</string>
|
||||
<string name="dark">Dark</string>
|
||||
<string name="application_preferences">Application Preferences</string>
|
||||
<string name="settings_section_appearance">Appearance</string>
|
||||
<string name="settings_section_media">Media & Data</string>
|
||||
<string name="settings_section_general">General</string>
|
||||
<string name="wallet_connect">Wallet Connect</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="theme">Theme</string>
|
||||
@@ -1972,6 +1981,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