add grouped feed filter dialog with Material 3 styling
add icons, reduce text size, center group headers in filter dialog
This commit is contained in:
+251
-58
@@ -21,20 +21,37 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.navigation.topbars
|
package com.vitorpamplona.amethyst.ui.navigation.topbars
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
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.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
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.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ExpandMore
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
|
import androidx.compose.material.icons.outlined.Groups
|
||||||
|
import androidx.compose.material.icons.outlined.LocationOn
|
||||||
|
import androidx.compose.material.icons.outlined.Person
|
||||||
|
import androidx.compose.material.icons.outlined.Public
|
||||||
|
import androidx.compose.material.icons.outlined.SensorDoor
|
||||||
|
import androidx.compose.material.icons.outlined.ViewList
|
||||||
|
import androidx.compose.material.icons.outlined.VolumeOff
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -43,14 +60,17 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
import androidx.compose.ui.semantics.onClick
|
import androidx.compose.ui.semantics.onClick
|
||||||
import androidx.compose.ui.semantics.role
|
import androidx.compose.ui.semantics.role
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.semantics.stateDescription
|
import androidx.compose.ui.semantics.stateDescription
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
import com.google.accompanist.permissions.isGranted
|
import com.google.accompanist.permissions.isGranted
|
||||||
@@ -62,7 +82,6 @@ import com.vitorpamplona.amethyst.model.TopFilter
|
|||||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNote
|
||||||
import com.vitorpamplona.amethyst.ui.components.LoadingAnimation
|
import com.vitorpamplona.amethyst.ui.components.LoadingAnimation
|
||||||
import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog
|
|
||||||
import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName
|
import com.vitorpamplona.amethyst.ui.note.creators.location.LoadCityName
|
||||||
import com.vitorpamplona.amethyst.ui.screen.CommunityName
|
import com.vitorpamplona.amethyst.ui.screen.CommunityName
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
import com.vitorpamplona.amethyst.ui.screen.FeedDefinition
|
||||||
@@ -217,7 +236,7 @@ fun FeedFilterSpinner(
|
|||||||
|
|
||||||
if (optionsShowing) {
|
if (optionsShowing) {
|
||||||
options.isNotEmpty().also {
|
options.isNotEmpty().also {
|
||||||
SpinnerSelectionDialog(
|
GroupedFeedFilterDialog(
|
||||||
title = explainer,
|
title = explainer,
|
||||||
options = options,
|
options = options,
|
||||||
onDismiss = { optionsShowing = false },
|
onDismiss = { optionsShowing = false },
|
||||||
@@ -241,84 +260,258 @@ fun RenderOption(
|
|||||||
when (option) {
|
when (option) {
|
||||||
is GeoHashName -> {
|
is GeoHashName -> {
|
||||||
LoadCityName(option.geoHashTag) {
|
LoadCityName(option.geoHashTag) {
|
||||||
Row(
|
Text(text = "/g/$it", fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurface)
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Text(text = "/g/$it", color = MaterialTheme.colorScheme.onSurface)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is HashtagName -> {
|
is HashtagName -> {
|
||||||
Row(
|
Text(text = option.name(), fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurface)
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
Text(text = option.name(), color = MaterialTheme.colorScheme.onSurface)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is ResourceName -> {
|
is ResourceName -> {
|
||||||
Row(
|
Text(
|
||||||
horizontalArrangement = Arrangement.Center,
|
text = stringRes(id = option.resourceId),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
fontSize = 14.sp,
|
||||||
) {
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
Text(
|
)
|
||||||
text = stringRes(id = option.resourceId),
|
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is PeopleListName -> {
|
is PeopleListName -> {
|
||||||
Row(
|
val noteState by observeNote(option.note, accountViewModel)
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
val noteState by observeNote(option.note, accountViewModel)
|
|
||||||
|
|
||||||
val noteEvent = noteState.note.event
|
val noteEvent = noteState.note.event
|
||||||
val name =
|
val name =
|
||||||
when (noteEvent) {
|
when (noteEvent) {
|
||||||
is PeopleListEvent -> {
|
is PeopleListEvent -> {
|
||||||
noteEvent.titleOrName() ?: option.note.dTag()
|
noteEvent.titleOrName() ?: option.note.dTag()
|
||||||
}
|
|
||||||
|
|
||||||
is FollowListEvent -> {
|
|
||||||
noteEvent.title() ?: option.note.dTag()
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
option.note.dTag()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(text = name, color = MaterialTheme.colorScheme.onSurface)
|
is FollowListEvent -> {
|
||||||
}
|
noteEvent.title() ?: option.note.dTag()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
option.note.dTag()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(text = name, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurface)
|
||||||
}
|
}
|
||||||
|
|
||||||
is CommunityName -> {
|
is CommunityName -> {
|
||||||
Row(
|
val it by observeNote(option.note, accountViewModel)
|
||||||
horizontalArrangement = Arrangement.Center,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
) {
|
|
||||||
val it by observeNote(option.note, accountViewModel)
|
|
||||||
|
|
||||||
Text(text = "/n/${((it.note as? AddressableNote)?.dTag() ?: "")}", color = MaterialTheme.colorScheme.onSurface)
|
Text(text = "/n/${((it.note as? AddressableNote)?.dTag() ?: "")}", fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurface)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is RelayName -> {
|
is RelayName -> {
|
||||||
Row(
|
Text(
|
||||||
horizontalArrangement = Arrangement.Center,
|
text = option.name(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
fontSize = 14.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
private data class IndexedFeedDefinition(
|
||||||
|
val originalIndex: Int,
|
||||||
|
val item: FeedDefinition,
|
||||||
|
)
|
||||||
|
|
||||||
|
private enum class FeedGroup(
|
||||||
|
val label: String,
|
||||||
|
) {
|
||||||
|
FEEDS("Feeds"),
|
||||||
|
HASHTAGS("Hashtags"),
|
||||||
|
COMMUNITIES("Communities"),
|
||||||
|
LISTS("Lists"),
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun groupFeedDefinitions(options: ImmutableList<FeedDefinition>): Map<FeedGroup, List<IndexedFeedDefinition>> {
|
||||||
|
val indexed = options.mapIndexed { index, item -> IndexedFeedDefinition(index, item) }
|
||||||
|
return indexed.groupBy { entry ->
|
||||||
|
when (entry.item.name) {
|
||||||
|
is HashtagName -> FeedGroup.HASHTAGS
|
||||||
|
is CommunityName -> FeedGroup.COMMUNITIES
|
||||||
|
is PeopleListName -> FeedGroup.LISTS
|
||||||
|
else -> FeedGroup.FEEDS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun GroupedFeedFilterDialog(
|
||||||
|
title: String,
|
||||||
|
options: ImmutableList<FeedDefinition>,
|
||||||
|
onSelect: (Int) -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onRenderItem: @Composable (FeedDefinition) -> Unit,
|
||||||
|
) {
|
||||||
|
val grouped = remember(options) { groupFeedDefinitions(options) }
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(28.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.padding(vertical = 20.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
item {
|
||||||
text = option.name(),
|
Text(
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
text = title,
|
||||||
)
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
FeedGroup.entries.forEach { group ->
|
||||||
|
val items = grouped[group]
|
||||||
|
if (!items.isNullOrEmpty()) {
|
||||||
|
item {
|
||||||
|
GroupSection(
|
||||||
|
label = group.label,
|
||||||
|
items = items,
|
||||||
|
isChipLayout = group == FeedGroup.HASHTAGS,
|
||||||
|
onSelect = onSelect,
|
||||||
|
onRenderItem = onRenderItem,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun GroupSection(
|
||||||
|
label: String,
|
||||||
|
items: List<IndexedFeedDefinition>,
|
||||||
|
isChipLayout: Boolean,
|
||||||
|
onSelect: (Int) -> Unit,
|
||||||
|
onRenderItem: @Composable (FeedDefinition) -> Unit,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = label.uppercase(),
|
||||||
|
fontSize = 12.sp,
|
||||||
|
letterSpacing = 0.8.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 6.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isChipLayout) {
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
|
) {
|
||||||
|
items.forEach { entry ->
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.clickable { onSelect(entry.originalIndex) },
|
||||||
|
shape = RoundedCornerShape(18.dp),
|
||||||
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
||||||
|
color = Color.Transparent,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = entry.item.name.name(),
|
||||||
|
fontSize = 13.sp,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.padding(horizontal = 14.dp, vertical = 7.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
} else {
|
||||||
|
items.forEach { entry ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onSelect(entry.originalIndex) }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 6.dp),
|
||||||
|
) {
|
||||||
|
FeedIcon(
|
||||||
|
item = entry.item,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.padding(start = 12.dp))
|
||||||
|
Column(modifier = Modifier.weight(1f)) { onRenderItem(entry.item) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FeedIcon(
|
||||||
|
item: FeedDefinition,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val icon =
|
||||||
|
when (item.code) {
|
||||||
|
is TopFilter.Global -> {
|
||||||
|
Icons.Outlined.Public
|
||||||
|
}
|
||||||
|
|
||||||
|
is TopFilter.AroundMe -> {
|
||||||
|
Icons.Outlined.LocationOn
|
||||||
|
}
|
||||||
|
|
||||||
|
is TopFilter.AllFollows -> {
|
||||||
|
Icons.Outlined.Groups
|
||||||
|
}
|
||||||
|
|
||||||
|
is TopFilter.AllUserFollows -> {
|
||||||
|
Icons.Outlined.Person
|
||||||
|
}
|
||||||
|
|
||||||
|
is TopFilter.DefaultFollows -> {
|
||||||
|
Icons.Outlined.Groups
|
||||||
|
}
|
||||||
|
|
||||||
|
is TopFilter.MuteList -> {
|
||||||
|
Icons.Outlined.VolumeOff
|
||||||
|
}
|
||||||
|
|
||||||
|
is TopFilter.Chess -> {
|
||||||
|
Icons.Outlined.Groups
|
||||||
|
}
|
||||||
|
|
||||||
|
is TopFilter.PeopleList -> {
|
||||||
|
Icons.Outlined.ViewList
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
when (item.name) {
|
||||||
|
is GeoHashName -> Icons.Outlined.LocationOn
|
||||||
|
is RelayName -> Icons.Outlined.SensorDoor
|
||||||
|
is CommunityName -> Icons.Outlined.Groups
|
||||||
|
is PeopleListName -> Icons.Outlined.ViewList
|
||||||
|
else -> Icons.Outlined.Person
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = modifier,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1545,7 +1545,7 @@
|
|||||||
<string name="my_lists_and_sets">My Lists/Sets</string>
|
<string name="my_lists_and_sets">My Lists/Sets</string>
|
||||||
<string name="my_lists">My Lists</string>
|
<string name="my_lists">My Lists</string>
|
||||||
<string name="people_list_label">Users</string>
|
<string name="people_list_label">Users</string>
|
||||||
<string name="select_list_to_filter">Select a list to filter the feed</string>
|
<string name="select_list_to_filter">Select an option to filter the feed</string>
|
||||||
|
|
||||||
<string name="temporary_account">Log off on device lock</string>
|
<string name="temporary_account">Log off on device lock</string>
|
||||||
<string name="private_message">Private Message</string>
|
<string name="private_message">Private Message</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user