refactor: address Security Filters audit findings
- Extract SettingsSection/SettingsItem/SettingsDivider/SettingsControlRow/
SettingsBlockTile/SettingsSubControlRow into SettingsSectionCard.kt.
AllSettingsScreen now consumes the shared components instead of carrying
its own private copies, and SecurityFiltersScreen drops the parallel set
it used to declare.
- BlockedContentRow folds into SettingsItem(trailing = { CountBadge(...) }),
removing the bespoke navigation row.
- Replace the Spinner/Switch tile mix with WarnReportsTile = SwitchTile +
SettingsSubControlRow(stepper), avoiding the nullable-icon code smell.
- Move the sensitive-content SegmentedButtonRow out of a cramped trailing
slot and into SettingsBlockTile so it has full-width room.
- HiddenWordsScreen: replace the eager Column.forEach with LazyColumn, and
add Modifier.imePadding() to the Scaffold so the keyboard no longer
covers the Add-word field.
- Move SelectableUserList from BlockedUsersScreen.kt to SecurityListsCommon
and drop the nullable onToggle (both call sites pass one).
- BlockListTopBar now takes selectedCount: Int instead of Set<*>; its
UserFeedState branches are exhaustive (no else -> Unit fallthrough).
- CountBadge reserves min width so 0↔n transitions don't shift the row.
- Stepper relies on Material 3's enabled handling instead of computing
alpha by hand and tinting LocalContentColor in three places.
- Distinct icons per tile/category (Visibility, Code, VisibilityOff, etc.)
so the same MaterialSymbols.Tag isn't reused for unrelated rows.
This commit is contained in:
-168
@@ -21,26 +21,15 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
||||||
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -51,26 +40,19 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
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.icons.symbols.MaterialSymbols
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.EmptyNav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
import com.vitorpamplona.amethyst.ui.navigation.routes.Route
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||||
import com.vitorpamplona.amethyst.ui.painterRes
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
@@ -371,153 +353,3 @@ private fun ResetMarmotStateDialog(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SettingsSection(
|
|
||||||
title: Int,
|
|
||||||
isDanger: Boolean = false,
|
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringRes(title),
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
color =
|
|
||||||
if (isDanger) {
|
|
||||||
MaterialTheme.colorScheme.error
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.primary
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(horizontal = 4.dp),
|
|
||||||
)
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(20.dp),
|
|
||||||
colors =
|
|
||||||
CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
|
||||||
),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
|
||||||
) {
|
|
||||||
Column(content = content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SettingsDivider() {
|
|
||||||
HorizontalDivider(
|
|
||||||
modifier = Modifier.padding(start = 68.dp),
|
|
||||||
thickness = 0.5.dp,
|
|
||||||
color = MaterialTheme.colorScheme.outlineVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SettingsItem(
|
|
||||||
title: Int,
|
|
||||||
icon: MaterialSymbol,
|
|
||||||
isDanger: Boolean = false,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
SettingsItemRow(
|
|
||||||
title = title,
|
|
||||||
isDanger = isDanger,
|
|
||||||
onClick = onClick,
|
|
||||||
leadingIcon = { tint ->
|
|
||||||
Icon(
|
|
||||||
symbol = icon,
|
|
||||||
contentDescription = stringRes(title),
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = tint,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SettingsItem(
|
|
||||||
title: Int,
|
|
||||||
iconPainter: Int,
|
|
||||||
iconPainterRef: Int,
|
|
||||||
isDanger: Boolean = false,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
val painter: Painter = painterRes(iconPainter, iconPainterRef)
|
|
||||||
SettingsItemRow(
|
|
||||||
title = title,
|
|
||||||
isDanger = isDanger,
|
|
||||||
onClick = onClick,
|
|
||||||
leadingIcon = { tint ->
|
|
||||||
Icon(
|
|
||||||
painter = painter,
|
|
||||||
contentDescription = stringRes(title),
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = tint,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SettingsItemRow(
|
|
||||||
title: Int,
|
|
||||||
isDanger: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
leadingIcon: @Composable (tint: Color) -> Unit,
|
|
||||||
) {
|
|
||||||
val containerColor =
|
|
||||||
if (isDanger) {
|
|
||||||
MaterialTheme.colorScheme.errorContainer
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.primaryContainer
|
|
||||||
}
|
|
||||||
val iconTint =
|
|
||||||
if (isDanger) {
|
|
||||||
MaterialTheme.colorScheme.onErrorContainer
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
}
|
|
||||||
val textColor =
|
|
||||||
if (isDanger) {
|
|
||||||
MaterialTheme.colorScheme.error
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.onSurface
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.size(36.dp)
|
|
||||||
.clip(RoundedCornerShape(10.dp))
|
|
||||||
.background(containerColor),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
leadingIcon(iconTint)
|
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = stringRes(title),
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = textColor,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(start = 16.dp),
|
|
||||||
)
|
|
||||||
Icon(
|
|
||||||
symbol = MaterialSymbols.ChevronRight,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-107
@@ -20,47 +20,19 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
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.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.material3.Checkbox
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
|
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.ShowUserButton
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.UserFeedState
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
|
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.HiddenAccountsFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.dal.HiddenAccountsFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size15dp
|
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BlockedUsersScreen(
|
fun BlockedUsersScreen(
|
||||||
@@ -78,7 +50,7 @@ fun BlockedUsersScreen(
|
|||||||
topBar = {
|
topBar = {
|
||||||
BlockListTopBar(
|
BlockListTopBar(
|
||||||
title = R.string.blocked_users,
|
title = R.string.blocked_users,
|
||||||
selected = selected,
|
selectedCount = selected.size,
|
||||||
onCancel = { selected = emptySet() },
|
onCancel = { selected = emptySet() },
|
||||||
onUnblock = {
|
onUnblock = {
|
||||||
accountViewModel.showUsers(selected.toList())
|
accountViewModel.showUsers(selected.toList())
|
||||||
@@ -99,81 +71,3 @@ fun BlockedUsersScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
internal fun SelectableUserList(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
viewModel: UserFeedViewModel,
|
|
||||||
emptyMessage: Int,
|
|
||||||
selected: Set<String>,
|
|
||||||
onToggle: ((String) -> Unit)? = null,
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
nav: INav,
|
|
||||||
) {
|
|
||||||
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
|
|
||||||
val selectionMode = selected.isNotEmpty()
|
|
||||||
|
|
||||||
when (val state = feedState) {
|
|
||||||
is UserFeedState.Loaded -> {
|
|
||||||
val items by state.feed.collectAsStateWithLifecycle()
|
|
||||||
val listState = rememberLazyListState()
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
modifier = modifier.fillMaxSize(),
|
|
||||||
contentPadding = rememberFeedContentPadding(FeedPadding),
|
|
||||||
state = listState,
|
|
||||||
) {
|
|
||||||
itemsIndexed(items, key = { _, item -> item.pubkeyHex }) { _, user ->
|
|
||||||
val isSelected = user.pubkeyHex in selected
|
|
||||||
val rowModifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.combinedClickable(
|
|
||||||
onClick = {
|
|
||||||
if (selectionMode && onToggle != null) {
|
|
||||||
onToggle(user.pubkeyHex)
|
|
||||||
} else {
|
|
||||||
nav.nav(routeFor(user))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLongClick = { onToggle?.invoke(user.pubkeyHex) },
|
|
||||||
).let {
|
|
||||||
if (isSelected) {
|
|
||||||
it.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f))
|
|
||||||
} else {
|
|
||||||
it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = rowModifier.padding(horizontal = Size15dp, vertical = Size10dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
UserPicture(user, Size55dp, accountViewModel = accountViewModel, nav = nav)
|
|
||||||
Column(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.padding(start = 10.dp)
|
|
||||||
.weight(1f),
|
|
||||||
) {
|
|
||||||
UsernameDisplay(user, accountViewModel = accountViewModel)
|
|
||||||
}
|
|
||||||
if (selectionMode) {
|
|
||||||
Checkbox(checked = isSelected, onCheckedChange = { onToggle?.invoke(user.pubkeyHex) })
|
|
||||||
} else {
|
|
||||||
ShowUserButton { accountViewModel.show(user) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HorizontalDivider(thickness = DividerThickness)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
EmptyOrLoading(modifier = modifier, state = state, emptyMessage = emptyMessage) {
|
|
||||||
viewModel.invalidateData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+12
-6
@@ -24,11 +24,14 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
@@ -79,10 +82,11 @@ fun HiddenWordsScreen(
|
|||||||
var selected by remember { mutableStateOf(setOf<String>()) }
|
var selected by remember { mutableStateOf(setOf<String>()) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
modifier = Modifier.imePadding(),
|
||||||
topBar = {
|
topBar = {
|
||||||
BlockListTopBar(
|
BlockListTopBar(
|
||||||
title = R.string.hidden_words,
|
title = R.string.hidden_words,
|
||||||
selected = selected,
|
selectedCount = selected.size,
|
||||||
onCancel = { selected = emptySet() },
|
onCancel = { selected = emptySet() },
|
||||||
onUnblock = {
|
onUnblock = {
|
||||||
if (!accountViewModel.isWriteable()) {
|
if (!accountViewModel.isWriteable()) {
|
||||||
@@ -109,7 +113,6 @@ fun HiddenWordsScreen(
|
|||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
selected = selected,
|
selected = selected,
|
||||||
onToggle = { selected = if (it in selected) selected - it else selected + it },
|
onToggle = { selected = if (it in selected) selected - it else selected + it },
|
||||||
accountViewModel = accountViewModel,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,7 +123,6 @@ private fun HiddenWordsList(
|
|||||||
viewModel: HiddenWordsFeedViewModel,
|
viewModel: HiddenWordsFeedViewModel,
|
||||||
selected: Set<String>,
|
selected: Set<String>,
|
||||||
onToggle: (String) -> Unit,
|
onToggle: (String) -> Unit,
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
) {
|
) {
|
||||||
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
|
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
@@ -130,8 +132,12 @@ private fun HiddenWordsList(
|
|||||||
if (items.isEmpty()) {
|
if (items.isEmpty()) {
|
||||||
EmptyState(modifier, R.string.security_hidden_words_empty)
|
EmptyState(modifier, R.string.security_hidden_words_empty)
|
||||||
} else {
|
} else {
|
||||||
Column(modifier = modifier.fillMaxSize()) {
|
val listState = rememberLazyListState()
|
||||||
items.forEach { word ->
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
state = listState,
|
||||||
|
) {
|
||||||
|
items(items, key = { it }) { word ->
|
||||||
MutedWordRow(
|
MutedWordRow(
|
||||||
tag = word,
|
tag = word,
|
||||||
isSelected = word in selected,
|
isSelected = word in selected,
|
||||||
|
|||||||
+85
-255
@@ -21,24 +21,18 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Card
|
|
||||||
import androidx.compose.material3.CardDefaults
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.LocalContentColor
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SegmentedButton
|
import androidx.compose.material3.SegmentedButton
|
||||||
@@ -51,7 +45,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
@@ -110,19 +104,18 @@ private fun SensitiveContentTile(accountViewModel: AccountViewModel) {
|
|||||||
val showSensitive by accountViewModel.account.settings.syncedSettings.security.showSensitiveContent
|
val showSensitive by accountViewModel.account.settings.syncedSettings.security.showSensitiveContent
|
||||||
.collectAsStateWithLifecycle()
|
.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
SettingsTile(
|
SettingsBlockTile(
|
||||||
icon = MaterialSymbols.VisibilityOff,
|
icon = MaterialSymbols.Visibility,
|
||||||
title = stringRes(R.string.show_sensitive_content_title),
|
title = stringRes(R.string.show_sensitive_content_title),
|
||||||
description = stringRes(R.string.show_sensitive_content_explainer),
|
description = stringRes(R.string.show_sensitive_content_explainer),
|
||||||
) {
|
) {
|
||||||
val current = parseWarningType(showSensitive)
|
val current = parseWarningType(showSensitive)
|
||||||
val options = listOf(WarningType.WARN, WarningType.SHOW, WarningType.HIDE)
|
|
||||||
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
|
||||||
options.forEachIndexed { index, type ->
|
WarningType.entries.forEachIndexed { index, type ->
|
||||||
SegmentedButton(
|
SegmentedButton(
|
||||||
selected = current == type,
|
selected = current == type,
|
||||||
onClick = { accountViewModel.updateShowSensitiveContent(type.prefCode) },
|
onClick = { accountViewModel.updateShowSensitiveContent(type.prefCode) },
|
||||||
shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size),
|
shape = SegmentedButtonDefaults.itemShape(index = index, count = WarningType.entries.size),
|
||||||
) {
|
) {
|
||||||
Text(stringRes(type.resourceId))
|
Text(stringRes(type.resourceId))
|
||||||
}
|
}
|
||||||
@@ -138,8 +131,8 @@ private fun FilterSpamTile(accountViewModel: AccountViewModel) {
|
|||||||
|
|
||||||
SwitchTile(
|
SwitchTile(
|
||||||
icon = MaterialSymbols.FilterAlt,
|
icon = MaterialSymbols.FilterAlt,
|
||||||
title = stringRes(R.string.filter_spam_from_strangers_title),
|
titleRes = R.string.filter_spam_from_strangers_title,
|
||||||
description = stringRes(R.string.filter_spam_from_strangers_explainer),
|
descriptionRes = R.string.filter_spam_from_strangers_explainer,
|
||||||
checked = filterSpam,
|
checked = filterSpam,
|
||||||
onCheckedChange = { accountViewModel.updateFilterSpam(it) },
|
onCheckedChange = { accountViewModel.updateFilterSpam(it) },
|
||||||
)
|
)
|
||||||
@@ -152,8 +145,8 @@ private fun HideCommunityViolationsTile(accountViewModel: AccountViewModel) {
|
|||||||
|
|
||||||
SwitchTile(
|
SwitchTile(
|
||||||
icon = MaterialSymbols.Shield,
|
icon = MaterialSymbols.Shield,
|
||||||
title = stringRes(R.string.hide_community_rules_violations_title),
|
titleRes = R.string.hide_community_rules_violations_title,
|
||||||
description = stringRes(R.string.hide_community_rules_violations_explainer),
|
descriptionRes = R.string.hide_community_rules_violations_explainer,
|
||||||
checked = hideViolations,
|
checked = hideViolations,
|
||||||
onCheckedChange = { accountViewModel.account.settings.changeHideCommunityRulesViolations(it) },
|
onCheckedChange = { accountViewModel.account.settings.changeHideCommunityRulesViolations(it) },
|
||||||
)
|
)
|
||||||
@@ -165,9 +158,9 @@ private fun DisableClientTagTile(accountViewModel: AccountViewModel) {
|
|||||||
.collectAsStateWithLifecycle()
|
.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
SwitchTile(
|
SwitchTile(
|
||||||
icon = MaterialSymbols.Tag,
|
icon = MaterialSymbols.Code,
|
||||||
title = stringRes(R.string.disable_client_tag_title),
|
titleRes = R.string.disable_client_tag_title,
|
||||||
description = stringRes(R.string.disable_client_tag_explainer),
|
descriptionRes = R.string.disable_client_tag_explainer,
|
||||||
checked = disableClientTag,
|
checked = disableClientTag,
|
||||||
onCheckedChange = { accountViewModel.updateDisableClientTag(it) },
|
onCheckedChange = { accountViewModel.updateDisableClientTag(it) },
|
||||||
)
|
)
|
||||||
@@ -180,30 +173,25 @@ private fun WarnReportsTile(accountViewModel: AccountViewModel) {
|
|||||||
val threshold by accountViewModel.account.settings.syncedSettings.security.reportWarningThreshold
|
val threshold by accountViewModel.account.settings.syncedSettings.security.reportWarningThreshold
|
||||||
.collectAsStateWithLifecycle()
|
.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Column {
|
SwitchTile(
|
||||||
SwitchTile(
|
icon = MaterialSymbols.Report,
|
||||||
icon = MaterialSymbols.Report,
|
titleRes = R.string.warn_when_posts_have_reports_from_your_follows_title,
|
||||||
title = stringRes(R.string.warn_when_posts_have_reports_from_your_follows_title),
|
descriptionRes = R.string.warn_when_posts_have_reports_from_your_follows_explainer,
|
||||||
description = stringRes(R.string.warn_when_posts_have_reports_from_your_follows_explainer),
|
checked = warnReports,
|
||||||
checked = warnReports,
|
onCheckedChange = { accountViewModel.updateWarnReports(it) },
|
||||||
onCheckedChange = { accountViewModel.updateWarnReports(it) },
|
)
|
||||||
)
|
SettingsSubControlRow(
|
||||||
|
title = stringRes(R.string.report_warning_threshold_title),
|
||||||
SettingsTile(
|
description = stringRes(R.string.report_warning_threshold_explainer),
|
||||||
icon = null,
|
enabled = warnReports,
|
||||||
title = stringRes(R.string.report_warning_threshold_title),
|
) {
|
||||||
description = stringRes(R.string.report_warning_threshold_explainer),
|
Stepper(
|
||||||
indented = true,
|
value = threshold.coerceAtLeast(1),
|
||||||
|
min = 1,
|
||||||
|
max = 999,
|
||||||
enabled = warnReports,
|
enabled = warnReports,
|
||||||
) {
|
onValueChange = { accountViewModel.updateReportWarningThreshold(it) },
|
||||||
Stepper(
|
)
|
||||||
value = threshold.coerceAtLeast(1),
|
|
||||||
min = 1,
|
|
||||||
max = 999,
|
|
||||||
enabled = warnReports,
|
|
||||||
onValueChange = { accountViewModel.updateReportWarningThreshold(it) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +200,7 @@ private fun MaxHashtagsTile(accountViewModel: AccountViewModel) {
|
|||||||
val maxHashtags by accountViewModel.account.settings.syncedSettings.security.maxHashtagLimit
|
val maxHashtags by accountViewModel.account.settings.syncedSettings.security.maxHashtagLimit
|
||||||
.collectAsStateWithLifecycle()
|
.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
SettingsTile(
|
SettingsControlRow(
|
||||||
icon = MaterialSymbols.Tag,
|
icon = MaterialSymbols.Tag,
|
||||||
title = stringRes(R.string.max_hashtag_limit_title),
|
title = stringRes(R.string.max_hashtag_limit_title),
|
||||||
description = stringRes(R.string.max_hashtag_limit_explainer),
|
description = stringRes(R.string.max_hashtag_limit_explainer),
|
||||||
@@ -227,6 +215,24 @@ private fun MaxHashtagsTile(accountViewModel: AccountViewModel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SwitchTile(
|
||||||
|
icon: MaterialSymbol,
|
||||||
|
titleRes: Int,
|
||||||
|
descriptionRes: Int,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
SettingsControlRow(
|
||||||
|
icon = icon,
|
||||||
|
title = stringRes(titleRes),
|
||||||
|
description = stringRes(descriptionRes),
|
||||||
|
onClick = { onCheckedChange(!checked) },
|
||||||
|
) {
|
||||||
|
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BlockedContentSection(
|
private fun BlockedContentSection(
|
||||||
accountViewModel: AccountViewModel,
|
accountViewModel: AccountViewModel,
|
||||||
@@ -236,224 +242,58 @@ private fun BlockedContentSection(
|
|||||||
.collectAsStateWithLifecycle()
|
.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
SettingsSection(R.string.security_section_blocked_content) {
|
SettingsSection(R.string.security_section_blocked_content) {
|
||||||
BlockedContentRow(
|
SettingsItem(
|
||||||
|
title = R.string.blocked_users,
|
||||||
icon = MaterialSymbols.PersonOff,
|
icon = MaterialSymbols.PersonOff,
|
||||||
title = stringRes(R.string.blocked_users),
|
trailing = { CountBadge(hidden.hiddenUsers.size) },
|
||||||
count = hidden.hiddenUsers.size,
|
|
||||||
onClick = { nav.nav(Route.BlockedUsers) },
|
onClick = { nav.nav(Route.BlockedUsers) },
|
||||||
)
|
)
|
||||||
SettingsDivider()
|
SettingsDivider()
|
||||||
BlockedContentRow(
|
SettingsItem(
|
||||||
|
title = R.string.spamming_users,
|
||||||
icon = MaterialSymbols.Block,
|
icon = MaterialSymbols.Block,
|
||||||
title = stringRes(R.string.spamming_users),
|
trailing = { CountBadge(hidden.spammers.size) },
|
||||||
count = hidden.spammers.size,
|
|
||||||
onClick = { nav.nav(Route.SpammingUsers) },
|
onClick = { nav.nav(Route.SpammingUsers) },
|
||||||
)
|
)
|
||||||
SettingsDivider()
|
SettingsDivider()
|
||||||
BlockedContentRow(
|
SettingsItem(
|
||||||
icon = MaterialSymbols.Tag,
|
title = R.string.hidden_words,
|
||||||
title = stringRes(R.string.hidden_words),
|
icon = MaterialSymbols.VisibilityOff,
|
||||||
count = hidden.hiddenWords.size,
|
trailing = { CountBadge(hidden.hiddenWords.size) },
|
||||||
onClick = { nav.nav(Route.HiddenWords) },
|
onClick = { nav.nav(Route.HiddenWords) },
|
||||||
)
|
)
|
||||||
SettingsDivider()
|
SettingsDivider()
|
||||||
BlockedContentRow(
|
SettingsItem(
|
||||||
|
title = R.string.settings_muted_threads_title,
|
||||||
icon = MaterialSymbols.Forum,
|
icon = MaterialSymbols.Forum,
|
||||||
title = stringRes(R.string.settings_muted_threads_title),
|
trailing = { CountBadge(hidden.mutedThreads.size) },
|
||||||
count = hidden.mutedThreads.size,
|
|
||||||
onClick = { nav.nav(Route.MutedThreads) },
|
onClick = { nav.nav(Route.MutedThreads) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
// Building blocks
|
|
||||||
// ──────────────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SettingsSection(
|
|
||||||
title: Int,
|
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
|
||||||
) {
|
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringRes(title),
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
color = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.padding(horizontal = 4.dp),
|
|
||||||
)
|
|
||||||
Card(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
shape = RoundedCornerShape(20.dp),
|
|
||||||
colors =
|
|
||||||
CardDefaults.cardColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
|
||||||
),
|
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
|
||||||
) {
|
|
||||||
Column(content = content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SettingsDivider() {
|
|
||||||
HorizontalDivider(
|
|
||||||
modifier = Modifier.padding(start = 68.dp),
|
|
||||||
thickness = 0.5.dp,
|
|
||||||
color = MaterialTheme.colorScheme.outlineVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SettingsTile(
|
|
||||||
icon: MaterialSymbol?,
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
indented: Boolean = false,
|
|
||||||
enabled: Boolean = true,
|
|
||||||
trailing: @Composable () -> Unit,
|
|
||||||
) {
|
|
||||||
val alpha = if (enabled) 1f else 0.5f
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(
|
|
||||||
start = if (indented) 56.dp else 16.dp,
|
|
||||||
end = 16.dp,
|
|
||||||
top = 12.dp,
|
|
||||||
bottom = 12.dp,
|
|
||||||
),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
if (icon != null) {
|
|
||||||
SettingsLeadingIcon(icon)
|
|
||||||
}
|
|
||||||
Column(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(start = if (icon != null) 16.dp else 0.dp, end = 12.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha),
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = description,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Box(contentAlignment = Alignment.Center) { trailing() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SwitchTile(
|
|
||||||
icon: MaterialSymbol,
|
|
||||||
title: String,
|
|
||||||
description: String,
|
|
||||||
checked: Boolean,
|
|
||||||
onCheckedChange: (Boolean) -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable { onCheckedChange(!checked) }
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
SettingsLeadingIcon(icon)
|
|
||||||
Column(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(start = 16.dp, end = 12.dp),
|
|
||||||
) {
|
|
||||||
Text(text = title, style = MaterialTheme.typography.bodyLarge)
|
|
||||||
Text(
|
|
||||||
text = description,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SettingsLeadingIcon(icon: MaterialSymbol) {
|
|
||||||
Box(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.size(36.dp)
|
|
||||||
.clip(RoundedCornerShape(10.dp))
|
|
||||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
symbol = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun BlockedContentRow(
|
|
||||||
icon: MaterialSymbol,
|
|
||||||
title: String,
|
|
||||||
count: Int,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable(onClick = onClick)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
SettingsLeadingIcon(icon)
|
|
||||||
Text(
|
|
||||||
text = title,
|
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(start = 16.dp),
|
|
||||||
)
|
|
||||||
CountBadge(count)
|
|
||||||
Icon(
|
|
||||||
symbol = MaterialSymbols.ChevronRight,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.padding(start = 8.dp).size(20.dp),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CountBadge(count: Int) {
|
private fun CountBadge(count: Int) {
|
||||||
if (count <= 0) return
|
// Reserve space so transitions to/from 0 don't shift the row.
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier = Modifier.widthIn(min = 28.dp),
|
||||||
Modifier
|
contentAlignment = Alignment.Center,
|
||||||
.clip(RoundedCornerShape(50))
|
|
||||||
.background(MaterialTheme.colorScheme.secondaryContainer)
|
|
||||||
.padding(horizontal = 10.dp, vertical = 2.dp),
|
|
||||||
) {
|
) {
|
||||||
Text(
|
if (count > 0) {
|
||||||
text = count.toString(),
|
Box(
|
||||||
style = MaterialTheme.typography.labelMedium,
|
modifier =
|
||||||
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
Modifier
|
||||||
)
|
.clip(RoundedCornerShape(50))
|
||||||
|
.background(MaterialTheme.colorScheme.secondaryContainer)
|
||||||
|
.padding(horizontal = 10.dp, vertical = 2.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = count.toString(),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,13 +306,11 @@ private fun Stepper(
|
|||||||
unsetLabel: String? = null,
|
unsetLabel: String? = null,
|
||||||
onValueChange: (Int) -> Unit,
|
onValueChange: (Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
val alpha = if (enabled) 1f else 0.5f
|
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.height(40.dp)
|
|
||||||
.clip(RoundedCornerShape(50))
|
.clip(RoundedCornerShape(50))
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainerHigh.copy(alpha = alpha)),
|
.background(MaterialTheme.colorScheme.surfaceContainerHigh),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -483,20 +321,13 @@ private fun Stepper(
|
|||||||
symbol = MaterialSymbols.Remove,
|
symbol = MaterialSymbols.Remove,
|
||||||
contentDescription = "−",
|
contentDescription = "−",
|
||||||
modifier = Modifier.size(18.dp),
|
modifier = Modifier.size(18.dp),
|
||||||
tint = LocalContentColor.current.copy(alpha = alpha),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val display =
|
|
||||||
if (unsetLabel != null && value <= 0) {
|
|
||||||
unsetLabel
|
|
||||||
} else {
|
|
||||||
value.toString()
|
|
||||||
}
|
|
||||||
Text(
|
Text(
|
||||||
text = display,
|
text = if (unsetLabel != null && value <= 0) unsetLabel else value.toString(),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha),
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.padding(horizontal = 4.dp),
|
modifier = Modifier.widthIn(min = 28.dp),
|
||||||
)
|
)
|
||||||
IconButton(
|
IconButton(
|
||||||
enabled = enabled && value < max,
|
enabled = enabled && value < max,
|
||||||
@@ -506,7 +337,6 @@ private fun Stepper(
|
|||||||
symbol = MaterialSymbols.Add,
|
symbol = MaterialSymbols.Add,
|
||||||
contentDescription = "+",
|
contentDescription = "+",
|
||||||
modifier = Modifier.size(18.dp),
|
modifier = Modifier.size(18.dp),
|
||||||
tint = LocalContentColor.current.copy(alpha = alpha),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+100
-11
@@ -20,14 +20,23 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
@@ -45,14 +54,25 @@ import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
|||||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
import com.vitorpamplona.amethyst.ui.feeds.FeedError
|
||||||
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
import com.vitorpamplona.amethyst.ui.feeds.LoadingFeed
|
||||||
|
import com.vitorpamplona.amethyst.ui.layouts.rememberFeedContentPadding
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.ShorterTopAppBar
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
import com.vitorpamplona.amethyst.ui.navigation.topbars.TopBarWithBackButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.ShowUserButton
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.UserPicture
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton
|
import com.vitorpamplona.amethyst.ui.note.buttons.CloseButton
|
||||||
import com.vitorpamplona.amethyst.ui.screen.UserFeedState
|
import com.vitorpamplona.amethyst.ui.screen.UserFeedState
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.stringRes
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
|
||||||
import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding
|
import com.vitorpamplona.amethyst.ui.theme.HalfHorzPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size15dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun InvalidateOnBlockListChange(
|
internal fun InvalidateOnBlockListChange(
|
||||||
@@ -73,18 +93,18 @@ internal fun InvalidateOnBlockListChange(
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun BlockListTopBar(
|
internal fun BlockListTopBar(
|
||||||
title: Int,
|
title: Int,
|
||||||
selected: Set<*>,
|
selectedCount: Int,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
onUnblock: () -> Unit,
|
onUnblock: () -> Unit,
|
||||||
nav: INav,
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
if (selected.isEmpty()) {
|
if (selectedCount == 0) {
|
||||||
TopBarWithBackButton(stringRes(id = title), nav)
|
TopBarWithBackButton(stringRes(id = title), nav)
|
||||||
} else {
|
} else {
|
||||||
ShorterTopAppBar(
|
ShorterTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = stringRes(R.string.num_selected, selected.size),
|
text = stringRes(R.string.num_selected, selectedCount),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
@@ -107,18 +127,87 @@ internal fun BlockListTopBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun EmptyOrLoading(
|
internal fun SelectableUserList(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
state: UserFeedState,
|
viewModel: UserFeedViewModel,
|
||||||
emptyMessage: Int,
|
emptyMessage: Int,
|
||||||
onRefresh: () -> Unit,
|
selected: Set<String>,
|
||||||
|
onToggle: (String) -> Unit,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
nav: INav,
|
||||||
) {
|
) {
|
||||||
when (state) {
|
val feedState by viewModel.feedContent.collectAsStateWithLifecycle()
|
||||||
is UserFeedState.Empty -> EmptyState(modifier, emptyMessage)
|
val selectionMode = selected.isNotEmpty()
|
||||||
is UserFeedState.Loading -> LoadingFeed()
|
|
||||||
is UserFeedState.FeedError -> FeedError(state.errorMessage, onRefresh)
|
when (val state = feedState) {
|
||||||
else -> Unit
|
is UserFeedState.Loaded -> {
|
||||||
|
val items by state.feed.collectAsStateWithLifecycle()
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
contentPadding = rememberFeedContentPadding(FeedPadding),
|
||||||
|
state = listState,
|
||||||
|
) {
|
||||||
|
items(items, key = { it.pubkeyHex }) { user ->
|
||||||
|
val isSelected = user.pubkeyHex in selected
|
||||||
|
val rowModifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = {
|
||||||
|
if (selectionMode) {
|
||||||
|
onToggle(user.pubkeyHex)
|
||||||
|
} else {
|
||||||
|
nav.nav(routeFor(user))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = { onToggle(user.pubkeyHex) },
|
||||||
|
).let {
|
||||||
|
if (isSelected) {
|
||||||
|
it.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.12f))
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = rowModifier.padding(horizontal = Size15dp, vertical = Size10dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
UserPicture(user, Size55dp, accountViewModel = accountViewModel, nav = nav)
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.padding(start = 10.dp)
|
||||||
|
.weight(1f),
|
||||||
|
) {
|
||||||
|
UsernameDisplay(user, accountViewModel = accountViewModel)
|
||||||
|
}
|
||||||
|
if (selectionMode) {
|
||||||
|
Checkbox(checked = isSelected, onCheckedChange = { onToggle(user.pubkeyHex) })
|
||||||
|
} else {
|
||||||
|
ShowUserButton { accountViewModel.show(user) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider(thickness = DividerThickness)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is UserFeedState.Empty -> {
|
||||||
|
EmptyState(modifier, emptyMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
is UserFeedState.Loading -> {
|
||||||
|
LoadingFeed()
|
||||||
|
}
|
||||||
|
|
||||||
|
is UserFeedState.FeedError -> {
|
||||||
|
FeedError(state.errorMessage) { viewModel.invalidateData() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+350
@@ -0,0 +1,350 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
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.ui.painterRes
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
|
import androidx.compose.material3.Icon as Material3Icon
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsSection(
|
||||||
|
title: Int,
|
||||||
|
isDanger: Boolean = false,
|
||||||
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(
|
||||||
|
text = stringRes(title),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color =
|
||||||
|
if (isDanger) {
|
||||||
|
MaterialTheme.colorScheme.error
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primary
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
|
)
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors =
|
||||||
|
CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
),
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||||
|
) {
|
||||||
|
Column(content = content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsDivider() {
|
||||||
|
HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(start = 68.dp),
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Navigation row: icon + title + optional `trailing` (count badge, etc.) + chevron. */
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsItem(
|
||||||
|
title: Int,
|
||||||
|
icon: MaterialSymbol,
|
||||||
|
isDanger: Boolean = false,
|
||||||
|
trailing: @Composable () -> Unit = {},
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
SettingsItemRow(
|
||||||
|
title = title,
|
||||||
|
isDanger = isDanger,
|
||||||
|
trailing = trailing,
|
||||||
|
onClick = onClick,
|
||||||
|
) { tint ->
|
||||||
|
Icon(
|
||||||
|
symbol = icon,
|
||||||
|
contentDescription = stringRes(title),
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = tint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Navigation row variant taking a drawable painter as the leading icon. */
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsItem(
|
||||||
|
title: Int,
|
||||||
|
iconPainter: Int,
|
||||||
|
iconPainterRef: Int,
|
||||||
|
isDanger: Boolean = false,
|
||||||
|
trailing: @Composable () -> Unit = {},
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val painter: Painter = painterRes(iconPainter, iconPainterRef)
|
||||||
|
SettingsItemRow(
|
||||||
|
title = title,
|
||||||
|
isDanger = isDanger,
|
||||||
|
trailing = trailing,
|
||||||
|
onClick = onClick,
|
||||||
|
) { tint ->
|
||||||
|
Material3Icon(
|
||||||
|
painter = painter,
|
||||||
|
contentDescription = stringRes(title),
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = tint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingsItemRow(
|
||||||
|
title: Int,
|
||||||
|
isDanger: Boolean,
|
||||||
|
trailing: @Composable () -> Unit,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
leadingIcon: @Composable (tint: Color) -> Unit,
|
||||||
|
) {
|
||||||
|
val containerColor =
|
||||||
|
if (isDanger) {
|
||||||
|
MaterialTheme.colorScheme.errorContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.primaryContainer
|
||||||
|
}
|
||||||
|
val iconTint =
|
||||||
|
if (isDanger) {
|
||||||
|
MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
}
|
||||||
|
val textColor =
|
||||||
|
if (isDanger) {
|
||||||
|
MaterialTheme.colorScheme.error
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
SettingsIconBox(containerColor) { leadingIcon(iconTint) }
|
||||||
|
Text(
|
||||||
|
text = stringRes(title),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = textColor,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
)
|
||||||
|
trailing()
|
||||||
|
Icon(
|
||||||
|
symbol = MaterialSymbols.ChevronRight,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.padding(start = 8.dp).size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Control row: icon + title + description, with an inline `trailing` control (switch, stepper).
|
||||||
|
* Clicking anywhere on the row invokes [onClick] — pass `null` to disable row clicks
|
||||||
|
* (useful when the control alone should toggle state).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsControlRow(
|
||||||
|
icon: MaterialSymbol,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
trailing: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val rowModifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.let { if (onClick != null && enabled) it.clickable(onClick = onClick) else it }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||||
|
val alpha = if (enabled) 1f else 0.5f
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = rowModifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
SettingsIconBox(MaterialTheme.colorScheme.primaryContainer) {
|
||||||
|
Icon(
|
||||||
|
symbol = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = alpha),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 16.dp, end = 12.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
trailing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* that only matters when the parent toggle is on).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsSubControlRow(
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
trailing: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val alpha = if (enabled) 1f else 0.5f
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 68.dp, end = 16.dp, top = 8.dp, bottom = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f).padding(end = 12.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = alpha),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
trailing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tile where the control sits below the title + description (e.g. a full-width
|
||||||
|
* SegmentedButtonRow). Use when the control would crowd a trailing slot.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun SettingsBlockTile(
|
||||||
|
icon: MaterialSymbol,
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
) {
|
||||||
|
SettingsIconBox(MaterialTheme.colorScheme.primaryContainer) {
|
||||||
|
Icon(
|
||||||
|
symbol = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Text(text = title, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SettingsIconBox(
|
||||||
|
containerColor: Color,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.clip(RoundedCornerShape(10.dp))
|
||||||
|
.background(containerColor),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
content = { content() },
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-1
@@ -50,7 +50,7 @@ fun SpammingUsersScreen(
|
|||||||
topBar = {
|
topBar = {
|
||||||
BlockListTopBar(
|
BlockListTopBar(
|
||||||
title = R.string.spamming_users,
|
title = R.string.spamming_users,
|
||||||
selected = selected,
|
selectedCount = selected.size,
|
||||||
onCancel = { selected = emptySet() },
|
onCancel = { selected = emptySet() },
|
||||||
onUnblock = {
|
onUnblock = {
|
||||||
accountViewModel.showUsers(selected.toList())
|
accountViewModel.showUsers(selected.toList())
|
||||||
|
|||||||
Reference in New Issue
Block a user