mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
feat(settings): modernize Privacy, Profile UI, Home Tabs & Calendar Reminder screens
Bring four settings screens onto the SettingsSection card kit used by the UI Preferences / Security Filters screens, and drop the last Save/Cancel flow in favor of auto-save + back-button-only navigation. - Privacy Options: remove SavingTopBar + TorDialogViewModel staging; bind controls directly to TorSettingsFlow.tryEmit (the existing debounced propertyWatchFlow already persists changes), so edits save on change and the screen has only a back arrow. Rebuild the body from SettingsSection cards: a segmented Tor-engine tile, an Orbot-port block, a live preset picker row, and per-usage switch tiles. - Profile UI & Home Tabs: reskin the ad-hoc rows into SettingsSection cards with SettingsSwitchTile / SegmentedChoiceTile (Home Tabs keeps the "can't disable the last tab" guard via the enabled flag). - Calendar Reminders: swap the hand-rolled TopAppBar for TopBarWithBackButton and reskin into a SettingsSection card with a segmented lead-time selector, preserving the CalendarReminderPrefs writes and WorkManager schedule/cancel side-effects. - Shared kit: make SegmentedChoiceTile internal for reuse and add a compact title-only SettingsSwitchTile overload with enabled support. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013JvrwLYbGVJQhabfZS7sBy
This commit is contained in:
+47
-71
@@ -20,37 +20,38 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
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.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SegmentedButton
|
||||
import androidx.compose.material3.SegmentedButtonDefaults
|
||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
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.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.service.calendar.CalendarReminderPrefs
|
||||
import com.vitorpamplona.amethyst.service.calendar.CalendarReminderWorker
|
||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsBlockTile
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsDivider
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSwitchTile
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -63,44 +64,25 @@ fun CalendarReminderSettingsScreen(nav: INav) {
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringRes(R.string.calendar_reminder_settings_title)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { nav.popBack() }) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.AutoMirrored.ArrowBack,
|
||||
contentDescription = stringRes(R.string.back),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
TopBarWithBackButton(stringRes(R.string.calendar_reminder_settings_title), nav)
|
||||
},
|
||||
) { pad ->
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.padding(pad)
|
||||
.padding(padding)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = stringRes(R.string.calendar_reminder_settings_enabled_title),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.calendar_reminder_settings_enabled_subtitle),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
SettingsSection(R.string.settings_section_reminders) {
|
||||
// The switch writes straight to the device-scoped prefs and re-schedules (or
|
||||
// cancels) the WorkManager job on the spot — no Save button, the change is applied
|
||||
// the instant it's toggled.
|
||||
SettingsSwitchTile(
|
||||
icon = MaterialSymbols.Notifications,
|
||||
title = R.string.calendar_reminder_settings_enabled_title,
|
||||
description = R.string.calendar_reminder_settings_enabled_subtitle,
|
||||
checked = enabled,
|
||||
onCheckedChange = {
|
||||
enabled = it
|
||||
@@ -115,35 +97,29 @@ fun CalendarReminderSettingsScreen(nav: INav) {
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = stringRes(R.string.calendar_reminder_settings_lead_title),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
Text(
|
||||
text = stringRes(R.string.calendar_reminder_settings_lead_subtitle),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
CalendarReminderPrefs.LEAD_TIME_CHOICES.forEach { choice ->
|
||||
FilterChip(
|
||||
selected = choice == leadMinutes,
|
||||
onClick = {
|
||||
leadMinutes = choice
|
||||
prefs.setLeadMinutes(choice)
|
||||
},
|
||||
enabled = enabled,
|
||||
label = {
|
||||
Text(pluralStringResource(R.plurals.calendar_reminder_settings_lead_choice, choice, choice))
|
||||
},
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsBlockTile(
|
||||
icon = MaterialSymbols.Schedule,
|
||||
title = stringRes(R.string.calendar_reminder_settings_lead_title),
|
||||
description = stringRes(R.string.calendar_reminder_settings_lead_subtitle),
|
||||
) {
|
||||
val choices = CalendarReminderPrefs.LEAD_TIME_CHOICES
|
||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||
choices.forEachIndexed { index, choice ->
|
||||
SegmentedButton(
|
||||
selected = choice == leadMinutes,
|
||||
enabled = enabled,
|
||||
onClick = {
|
||||
leadMinutes = choice
|
||||
prefs.setLeadMinutes(choice)
|
||||
},
|
||||
shape = SegmentedButtonDefaults.itemShape(index = index, count = choices.size),
|
||||
icon = {},
|
||||
) {
|
||||
Text(pluralStringResource(R.plurals.calendar_reminder_settings_lead_choice, choice, choice))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+269
-48
@@ -20,26 +20,71 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.privacy
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorSettings
|
||||
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.commons.tor.TorPresetType
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorType
|
||||
import com.vitorpamplona.amethyst.commons.tor.torDefaultPreset
|
||||
import com.vitorpamplona.amethyst.commons.tor.torFullyPrivate
|
||||
import com.vitorpamplona.amethyst.commons.tor.torOnlyWhenNeededPreset
|
||||
import com.vitorpamplona.amethyst.commons.tor.torSmallPayloadsPreset
|
||||
import com.vitorpamplona.amethyst.commons.tor.whichPreset
|
||||
import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog
|
||||
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.SavingTopBar
|
||||
import com.vitorpamplona.amethyst.ui.tor.PrivacySettingsBody
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorDialogViewModel
|
||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SegmentedChoiceTile
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsBlockTile
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsControlRow
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsDivider
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSection
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsSwitchTile
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.amethyst.ui.tor.explainerId
|
||||
import com.vitorpamplona.amethyst.ui.tor.resourceId
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -50,54 +95,230 @@ fun PrivacyOptionsScreen(nav: INav) {
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PrivacyOptionsScreen(
|
||||
torSettingsFlow: TorSettingsFlow,
|
||||
nav: INav,
|
||||
) {
|
||||
val dialogViewModel = viewModel<TorDialogViewModel>()
|
||||
|
||||
// runs only once and before the rest of the screen is build
|
||||
// to avoid blinking and animations from the default/previous
|
||||
// state to the current state
|
||||
val init =
|
||||
remember(dialogViewModel) {
|
||||
val torSettings = torSettingsFlow.toSettings()
|
||||
dialogViewModel.reset(torSettings)
|
||||
torSettings
|
||||
}
|
||||
|
||||
PrivacyOptionsScreenContents(dialogViewModel, onPost = torSettingsFlow::update, nav)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PrivacyOptionsScreenContents(
|
||||
dialogViewModel: TorDialogViewModel,
|
||||
onPost: (TorSettings) -> Unit,
|
||||
torSettings: TorSettingsFlow,
|
||||
nav: INav,
|
||||
) {
|
||||
Scaffold(
|
||||
topBar = {
|
||||
SavingTopBar(
|
||||
titleRes = R.string.privacy_options,
|
||||
onCancel = {
|
||||
nav.popBack()
|
||||
},
|
||||
onPost = {
|
||||
onPost(dialogViewModel.save())
|
||||
nav.popBack()
|
||||
},
|
||||
)
|
||||
},
|
||||
) {
|
||||
Column(
|
||||
Modifier
|
||||
.padding(it)
|
||||
topBar = { TopBarWithBackButton(stringRes(R.string.privacy_options), nav) },
|
||||
) { padding ->
|
||||
PrivacyOptionsContent(torSettings, Modifier.padding(padding))
|
||||
}
|
||||
}
|
||||
|
||||
// Every control writes straight to [TorSettingsFlow] via `tryEmit`; a debounced collector in
|
||||
// TorSharedPreferences persists the change automatically, so this screen has no Save/Cancel — the
|
||||
// back arrow is the only chrome and the state is already saved by the time the user leaves.
|
||||
@Composable
|
||||
fun PrivacyOptionsContent(
|
||||
torSettings: TorSettingsFlow,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val torType by torSettings.torType.collectAsStateWithLifecycle()
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(
|
||||
rememberScrollState(),
|
||||
).padding(horizontal = 10.dp),
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
) {
|
||||
SettingsSection(R.string.settings_section_tor_engine) {
|
||||
TorEngineTile(torSettings, torType)
|
||||
AnimatedVisibility(
|
||||
visible = torType == TorType.EXTERNAL,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
Column {
|
||||
SettingsDivider()
|
||||
OrbotPortTile(torSettings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = torType != TorType.OFF,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = fadeOut() + shrinkVertically(),
|
||||
) {
|
||||
PrivacySettingsBody(dialogViewModel)
|
||||
Column {
|
||||
Spacer(Modifier.height(20.dp))
|
||||
SettingsSection(R.string.settings_section_tor_routing) {
|
||||
PresetTile(torSettings)
|
||||
SettingsDivider()
|
||||
TorSwitchTile(torSettings.onionRelaysViaTor, MaterialSymbols.Public, R.string.tor_use_onion_address, R.string.tor_use_onion_address_explainer)
|
||||
SettingsDivider()
|
||||
TorSwitchTile(torSettings.dmRelaysViaTor, MaterialSymbols.Mail, R.string.tor_use_dm_relays, R.string.tor_use_dm_relays_explainer)
|
||||
SettingsDivider()
|
||||
TorSwitchTile(torSettings.newRelaysViaTor, MaterialSymbols.Dns, R.string.tor_use_new_relays, R.string.tor_use_new_relays_explainer)
|
||||
SettingsDivider()
|
||||
TorSwitchTile(torSettings.trustedRelaysViaTor, MaterialSymbols.Shield, R.string.tor_use_trusted_relays, R.string.tor_use_trusted_relays_explainer)
|
||||
SettingsDivider()
|
||||
TorSwitchTile(torSettings.moneyOperationsViaTor, MaterialSymbols.CurrencyBitcoin, R.string.tor_use_money_operations, R.string.tor_use_money_operations_explainer)
|
||||
SettingsDivider()
|
||||
TorSwitchTile(torSettings.nip05VerificationsViaTor, MaterialSymbols.AlternateEmail, R.string.tor_use_nip05_verification, R.string.tor_use_nip05_verification_explainer)
|
||||
SettingsDivider()
|
||||
TorSwitchTile(torSettings.urlPreviewsViaTor, MaterialSymbols.Link, R.string.tor_use_url_previews, R.string.tor_use_url_previews_explainer)
|
||||
SettingsDivider()
|
||||
TorSwitchTile(torSettings.imagesViaTor, MaterialSymbols.Image, R.string.tor_use_images, R.string.tor_use_images_explainer)
|
||||
SettingsDivider()
|
||||
TorSwitchTile(torSettings.videosViaTor, MaterialSymbols.Videocam, R.string.tor_use_videos, R.string.tor_use_videos_explainer)
|
||||
SettingsDivider()
|
||||
TorSwitchTile(torSettings.mediaUploadsViaTor, MaterialSymbols.CloudUpload, R.string.tor_use_media_uploads, R.string.tor_use_media_uploads_explainer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TorEngineTile(
|
||||
torSettings: TorSettingsFlow,
|
||||
torType: TorType,
|
||||
) {
|
||||
SegmentedChoiceTile(
|
||||
icon = MaterialSymbols.Security,
|
||||
title = R.string.use_internal_tor,
|
||||
description = R.string.use_internal_tor_explainer,
|
||||
options = TorType.entries,
|
||||
labelRes = { it.resourceId },
|
||||
selected = torType,
|
||||
onSelect = { torSettings.torType.tryEmit(it) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OrbotPortTile(torSettings: TorSettingsFlow) {
|
||||
val port by torSettings.externalSocksPort.collectAsStateWithLifecycle()
|
||||
// Local text state so the field can hold a transient, mid-typing value; only a value that
|
||||
// parses to a valid Int is pushed to the flow (and thereby saved).
|
||||
var text by remember { mutableStateOf(port.toString()) }
|
||||
|
||||
SettingsBlockTile(
|
||||
icon = MaterialSymbols.Dns,
|
||||
title = stringRes(R.string.orbot_socks_port),
|
||||
description = stringRes(R.string.connect_through_your_orbot_setup_short),
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = text,
|
||||
onValueChange = {
|
||||
text = it
|
||||
it.toIntOrNull()?.let { validPort -> torSettings.externalSocksPort.tryEmit(validPort) }
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions.Default.copy(
|
||||
capitalization = KeyboardCapitalization.None,
|
||||
keyboardType = KeyboardType.Number,
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "9050",
|
||||
color = MaterialTheme.colorScheme.placeholderText,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PresetTile(torSettings: TorSettingsFlow) {
|
||||
// propertyWatchFlow re-emits a full TorSettings on every toggle change, so the preset label
|
||||
// re-derives live (flipping to "Custom" the moment the flags stop matching a named preset).
|
||||
val current by torSettings.propertyWatchFlow.collectAsStateWithLifecycle(initialValue = torSettings.toSettings())
|
||||
val preset = whichPreset(current)
|
||||
var showPicker by remember { mutableStateOf(false) }
|
||||
|
||||
SettingsControlRow(
|
||||
icon = MaterialSymbols.Tune,
|
||||
title = stringRes(R.string.tor_preset),
|
||||
description = stringRes(R.string.tor_preset_explainer),
|
||||
onClick = { showPicker = true },
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = stringRes(preset.resourceId),
|
||||
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) {
|
||||
val options =
|
||||
TorPresetType.entries
|
||||
.map { TitleExplainer(stringRes(it.resourceId), stringRes(it.explainerId)) }
|
||||
.toImmutableList()
|
||||
SpinnerSelectionDialog(
|
||||
options = options,
|
||||
onDismiss = { showPicker = false },
|
||||
) { index ->
|
||||
showPicker = false
|
||||
torSettings.applyPreset(TorPresetType.entries[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TorSwitchTile(
|
||||
flow: MutableStateFlow<Boolean>,
|
||||
icon: MaterialSymbol,
|
||||
title: Int,
|
||||
description: Int,
|
||||
) {
|
||||
val checked by flow.collectAsStateWithLifecycle()
|
||||
SettingsSwitchTile(
|
||||
icon = icon,
|
||||
title = title,
|
||||
description = description,
|
||||
checked = checked,
|
||||
onCheckedChange = { flow.tryEmit(it) },
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a named preset by pushing its per-usage flags into the flow (auto-saving each). Only the
|
||||
* routing flags are touched — the engine choice and Orbot port are left as the user set them.
|
||||
* [TorPresetType.CUSTOM] is a no-op: it isn't a preset to apply, it's what the flags read as when
|
||||
* they match none of the named presets.
|
||||
*/
|
||||
private fun TorSettingsFlow.applyPreset(preset: TorPresetType) {
|
||||
val settings =
|
||||
when (preset) {
|
||||
TorPresetType.ONLY_WHEN_NEEDED -> torOnlyWhenNeededPreset
|
||||
TorPresetType.DEFAULT -> torDefaultPreset
|
||||
TorPresetType.SMALL_PAYLOADS -> torSmallPayloadsPreset
|
||||
TorPresetType.FULL_PRIVACY -> torFullyPrivate
|
||||
TorPresetType.CUSTOM -> return
|
||||
}
|
||||
onionRelaysViaTor.tryEmit(settings.onionRelaysViaTor)
|
||||
dmRelaysViaTor.tryEmit(settings.dmRelaysViaTor)
|
||||
newRelaysViaTor.tryEmit(settings.newRelaysViaTor)
|
||||
trustedRelaysViaTor.tryEmit(settings.trustedRelaysViaTor)
|
||||
urlPreviewsViaTor.tryEmit(settings.urlPreviewsViaTor)
|
||||
profilePicsViaTor.tryEmit(settings.profilePicsViaTor)
|
||||
imagesViaTor.tryEmit(settings.imagesViaTor)
|
||||
videosViaTor.tryEmit(settings.videosViaTor)
|
||||
moneyOperationsViaTor.tryEmit(settings.moneyOperationsViaTor)
|
||||
nip05VerificationsViaTor.tryEmit(settings.nip05VerificationsViaTor)
|
||||
mediaUploadsViaTor.tryEmit(settings.mediaUploadsViaTor)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Preview
|
||||
@Composable
|
||||
fun PrivacyOptionsScreenPreview() {
|
||||
ThemeComparisonRow {
|
||||
PrivacyOptionsScreen(TorSettingsFlow(), EmptyNav())
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -189,7 +189,7 @@ fun SettingsScreen(
|
||||
* choices instead of only naming them.
|
||||
*/
|
||||
@Composable
|
||||
private fun <T> SegmentedChoiceTile(
|
||||
internal fun <T> SegmentedChoiceTile(
|
||||
icon: MaterialSymbol,
|
||||
@StringRes title: Int,
|
||||
@StringRes description: Int,
|
||||
|
||||
+40
-90
@@ -20,36 +20,28 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||
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.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
|
||||
@Preview
|
||||
@@ -73,16 +65,15 @@ fun HomeTabsSettingsScreen(
|
||||
TopBarWithBackButton(stringRes(id = R.string.home_tabs_settings), nav)
|
||||
},
|
||||
) { padding ->
|
||||
Column(Modifier.padding(padding)) {
|
||||
HomeTabsSettingsContent(accountViewModel)
|
||||
}
|
||||
HomeTabsSettingsContent(accountViewModel.settings.uiSettingsFlow, Modifier.padding(padding))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HomeTabsSettingsContent(accountViewModel: AccountViewModel) {
|
||||
val ui = accountViewModel.settings.uiSettingsFlow
|
||||
|
||||
fun HomeTabsSettingsContent(
|
||||
ui: UiSettingsFlow,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val showNewThreads by ui.showHomeNewThreadsTab.collectAsStateWithLifecycle()
|
||||
val showConversations by ui.showHomeConversationsTab.collectAsStateWithLifecycle()
|
||||
val showEverything by ui.showHomeEverythingTab.collectAsStateWithLifecycle()
|
||||
@@ -91,78 +82,37 @@ fun HomeTabsSettingsContent(accountViewModel: AccountViewModel) {
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringRes(R.string.home_tabs_settings_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.padding(bottom = 16.dp, start = Size20dp, end = Size20dp),
|
||||
)
|
||||
|
||||
HomeTabSwitchRow(
|
||||
title = stringRes(R.string.new_threads),
|
||||
checked = showNewThreads,
|
||||
// Don't allow disabling the last remaining tab.
|
||||
enabled = !(showNewThreads && activeCount == 1),
|
||||
onCheckedChange = {
|
||||
ui.showHomeNewThreadsTab.tryEmit(it)
|
||||
},
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp))
|
||||
|
||||
HomeTabSwitchRow(
|
||||
title = stringRes(R.string.conversations),
|
||||
checked = showConversations,
|
||||
enabled = !(showConversations && activeCount == 1),
|
||||
onCheckedChange = {
|
||||
ui.showHomeConversationsTab.tryEmit(it)
|
||||
},
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp))
|
||||
|
||||
HomeTabSwitchRow(
|
||||
title = stringRes(R.string.home_tab_everything),
|
||||
checked = showEverything,
|
||||
enabled = !(showEverything && activeCount == 1),
|
||||
onCheckedChange = {
|
||||
ui.showHomeEverythingTab.tryEmit(it)
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HomeTabSwitchRow(
|
||||
title: String,
|
||||
checked: Boolean,
|
||||
enabled: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp, horizontal = Size20dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Switch(
|
||||
checked = checked,
|
||||
enabled = enabled,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
SettingsSection(R.string.settings_section_home_tabs) {
|
||||
SettingsSwitchTile(
|
||||
icon = MaterialSymbols.Forum,
|
||||
title = R.string.new_threads,
|
||||
checked = showNewThreads,
|
||||
// Don't allow disabling the last remaining tab.
|
||||
enabled = !(showNewThreads && activeCount == 1),
|
||||
onCheckedChange = { ui.showHomeNewThreadsTab.tryEmit(it) },
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsSwitchTile(
|
||||
icon = MaterialSymbols.Chat,
|
||||
title = R.string.conversations,
|
||||
checked = showConversations,
|
||||
enabled = !(showConversations && activeCount == 1),
|
||||
onCheckedChange = { ui.showHomeConversationsTab.tryEmit(it) },
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsSwitchTile(
|
||||
icon = MaterialSymbols.Public,
|
||||
title = R.string.home_tab_everything,
|
||||
checked = showEverything,
|
||||
enabled = !(showEverything && activeCount == 1),
|
||||
onCheckedChange = { ui.showHomeEverythingTab.tryEmit(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+60
-118
@@ -20,42 +20,30 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.model.ProfileGalleryType
|
||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||
import com.vitorpamplona.amethyst.model.parseGalleryType
|
||||
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.screen.loggedIn.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@@ -78,123 +66,77 @@ fun ProfileUiSettingsScreen(
|
||||
TopBarWithBackButton(stringRes(id = R.string.profile_ui_settings), nav)
|
||||
},
|
||||
) { padding ->
|
||||
Column(Modifier.padding(padding)) {
|
||||
ProfileUiSettingsContent(accountViewModel)
|
||||
}
|
||||
ProfileUiSettingsContent(accountViewModel.settings.uiSettingsFlow, Modifier.padding(padding))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileUiSettingsContent(accountViewModel: AccountViewModel) {
|
||||
val ui = accountViewModel.settings.uiSettingsFlow
|
||||
|
||||
fun ProfileUiSettingsContent(
|
||||
ui: UiSettingsFlow,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val showBadges by ui.showProfileBadges.collectAsStateWithLifecycle()
|
||||
val showAppRecommendations by ui.showProfileAppRecommendations.collectAsStateWithLifecycle()
|
||||
val showZapReceived by ui.showProfileZapReceivedFeed.collectAsStateWithLifecycle()
|
||||
val showFollowers by ui.showProfileFollowersFeed.collectAsStateWithLifecycle()
|
||||
val showOnchainWallet by ui.showOnchainWallet.collectAsStateWithLifecycle()
|
||||
val gallery by ui.gallerySet.collectAsStateWithLifecycle()
|
||||
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
) {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = stringRes(R.string.profile_ui_settings_description),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.padding(bottom = 16.dp, start = Size20dp, end = Size20dp),
|
||||
)
|
||||
|
||||
ProfileUiSwitchRow(
|
||||
title = stringRes(R.string.profile_ui_setting_badges),
|
||||
checked = showBadges,
|
||||
onCheckedChange = { ui.showProfileBadges.tryEmit(it) },
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp))
|
||||
|
||||
ProfileUiSwitchRow(
|
||||
title = stringRes(R.string.profile_ui_setting_app_recommendations),
|
||||
checked = showAppRecommendations,
|
||||
onCheckedChange = { ui.showProfileAppRecommendations.tryEmit(it) },
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp))
|
||||
|
||||
ProfileUiSwitchRow(
|
||||
title = stringRes(R.string.profile_ui_setting_zap_received_feed),
|
||||
checked = showZapReceived,
|
||||
onCheckedChange = { ui.showProfileZapReceivedFeed.tryEmit(it) },
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp))
|
||||
|
||||
ProfileUiSwitchRow(
|
||||
title = stringRes(R.string.profile_ui_setting_followers_feed),
|
||||
checked = showFollowers,
|
||||
onCheckedChange = { ui.showProfileFollowersFeed.tryEmit(it) },
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp))
|
||||
|
||||
ProfileUiSwitchRow(
|
||||
title = stringRes(R.string.profile_ui_setting_onchain_wallet),
|
||||
checked = showOnchainWallet,
|
||||
onCheckedChange = { ui.showOnchainWallet.tryEmit(it) },
|
||||
)
|
||||
HorizontalDivider(modifier = Modifier.padding(horizontal = Size20dp))
|
||||
|
||||
Column(modifier = Modifier.padding(vertical = 12.dp, horizontal = Size20dp)) {
|
||||
GalleryChoice(ui)
|
||||
SettingsSection(R.string.settings_section_profile_sections) {
|
||||
SettingsSwitchTile(
|
||||
icon = MaterialSymbols.MilitaryTech,
|
||||
title = R.string.profile_ui_setting_badges,
|
||||
checked = showBadges,
|
||||
onCheckedChange = { ui.showProfileBadges.tryEmit(it) },
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsSwitchTile(
|
||||
icon = MaterialSymbols.Recommend,
|
||||
title = R.string.profile_ui_setting_app_recommendations,
|
||||
checked = showAppRecommendations,
|
||||
onCheckedChange = { ui.showProfileAppRecommendations.tryEmit(it) },
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsSwitchTile(
|
||||
icon = MaterialSymbols.Bolt,
|
||||
title = R.string.profile_ui_setting_zap_received_feed,
|
||||
checked = showZapReceived,
|
||||
onCheckedChange = { ui.showProfileZapReceivedFeed.tryEmit(it) },
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsSwitchTile(
|
||||
icon = MaterialSymbols.Group,
|
||||
title = R.string.profile_ui_setting_followers_feed,
|
||||
checked = showFollowers,
|
||||
onCheckedChange = { ui.showProfileFollowersFeed.tryEmit(it) },
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsSwitchTile(
|
||||
icon = MaterialSymbols.AccountBalanceWallet,
|
||||
title = R.string.profile_ui_setting_onchain_wallet,
|
||||
checked = showOnchainWallet,
|
||||
onCheckedChange = { ui.showOnchainWallet.tryEmit(it) },
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GalleryChoice(sharedPrefs: UiSettingsFlow) {
|
||||
val galleryItems =
|
||||
persistentListOf(
|
||||
TitleExplainer(stringRes(ProfileGalleryType.CLASSIC.resourceId)),
|
||||
TitleExplainer(stringRes(ProfileGalleryType.MODERN.resourceId)),
|
||||
)
|
||||
|
||||
val galleryIndex by sharedPrefs.gallerySet.collectAsStateWithLifecycle()
|
||||
|
||||
SettingsRow(
|
||||
R.string.gallery_style,
|
||||
R.string.gallery_style_description,
|
||||
galleryItems,
|
||||
galleryIndex.screenCode,
|
||||
) {
|
||||
sharedPrefs.gallerySet.tryEmit(parseGalleryType(it))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileUiSwitchRow(
|
||||
title: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp, horizontal = Size20dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
)
|
||||
SettingsSection(R.string.settings_section_appearance) {
|
||||
SegmentedChoiceTile(
|
||||
icon = MaterialSymbols.Collections,
|
||||
title = R.string.gallery_style,
|
||||
description = R.string.gallery_style_description,
|
||||
options = ProfileGalleryType.entries,
|
||||
labelRes = { it.resourceId },
|
||||
selected = gallery,
|
||||
onSelect = { ui.gallerySet.tryEmit(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+44
@@ -272,6 +272,50 @@ internal fun SettingsSwitchTile(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact switch row: icon + title + trailing [Switch], with no description line — for
|
||||
* self-explanatory single-line toggles (e.g. "Profile Badges", "New Threads"). Tapping
|
||||
* anywhere on the row toggles. When [enabled] is false the row dims and stops responding
|
||||
* (used to lock the last remaining item in a "keep at least one on" group).
|
||||
*/
|
||||
@Composable
|
||||
internal fun SettingsSwitchTile(
|
||||
icon: MaterialSymbol,
|
||||
@StringRes title: Int,
|
||||
checked: Boolean,
|
||||
enabled: Boolean = true,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
) {
|
||||
val alpha = if (enabled) 1f else 0.5f
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.let { if (enabled) it.clickable { onCheckedChange(!checked) } else it }
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
SettingsIconBox(MaterialTheme.colorScheme.primaryContainer) {
|
||||
Icon(
|
||||
symbol = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = alpha),
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringRes(title),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha),
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp, end = 12.dp),
|
||||
)
|
||||
Switch(checked = checked, enabled = enabled, onCheckedChange = onCheckedChange)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sub-row variant of [SettingsControlRow]: indented in place of a leading icon,
|
||||
* used for controls hierarchically grouped under the row above (e.g. a threshold
|
||||
|
||||
@@ -2182,6 +2182,11 @@
|
||||
<string name="settings_section_appearance">Appearance</string>
|
||||
<string name="settings_section_media">Media & Data</string>
|
||||
<string name="settings_section_general">General</string>
|
||||
<string name="settings_section_tor_engine">Connection</string>
|
||||
<string name="settings_section_tor_routing">Route through Tor</string>
|
||||
<string name="settings_section_profile_sections">Sections & feeds</string>
|
||||
<string name="settings_section_home_tabs">Visible tabs</string>
|
||||
<string name="settings_section_reminders">Reminders</string>
|
||||
<string name="wallet_connect">Wallet Connect</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="theme">Theme</string>
|
||||
|
||||
Reference in New Issue
Block a user