From e73715f64a0d2c02437bd1c3fce2b4596981d64b Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 14 May 2026 21:21:55 +0200 Subject: [PATCH 1/2] =?UTF-8?q?Layout=20changes:=20pin=20"Add=20more?= =?UTF-8?q?=E2=80=A6"=20button=20to=20bottom=20of=20populated=20favourites?= =?UTF-8?q?=20screen=20group=20empty-state=20icon/headline/steps=20and=20a?= =?UTF-8?q?dd=20top=20breathing=20room=20spread=20empty-state=20evenly=20a?= =?UTF-8?q?nd=20add=20numbered=20step=20prefixes=20replace=20empty-state?= =?UTF-8?q?=20subtitle=20with=202-step=20instruction=20including=20inline?= =?UTF-8?q?=20star=20icon=20hide=20explainer=20when=20favorite=20feeds=20e?= =?UTF-8?q?xist=20and=20add=20"Add=20more=E2=80=A6"=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../favorites/FavoriteAlgoFeedsListScreen.kt | 137 +++++++++++++++--- amethyst/src/main/res/values/strings.xml | 3 + 2 files changed, 116 insertions(+), 24 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/favorites/FavoriteAlgoFeedsListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/favorites/FavoriteAlgoFeedsListScreen.kt index 3fdefd677..6b5837cd8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/favorites/FavoriteAlgoFeedsListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/favorites/FavoriteAlgoFeedsListScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.combinedClickable 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.Spacer import androidx.compose.foundation.layout.consumeWindowInsets @@ -37,6 +38,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material3.Button import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -48,10 +51,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.BottomStart import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -82,6 +89,8 @@ fun FavoriteAlgoFeedsListScreen( ) { val listState = rememberLazyListState() val coroutineScope = rememberCoroutineScope() + val favorites by accountViewModel.account.favoriteAlgoFeedsList.flowNotes + .collectAsStateWithLifecycle() DisappearingScaffold( isInvertedLayout = false, @@ -113,33 +122,34 @@ fun FavoriteAlgoFeedsListScreen( bottom = padding.calculateBottomPadding(), ).consumeWindowInsets(padding), ) { - Text( - text = stringRes(R.string.favorite_dvms_explainer), - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.grayText, - ) + if (favorites.isEmpty()) { + Text( + text = stringRes(R.string.favorite_dvms_explainer), + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth().padding(top = 24.dp, bottom = 12.dp), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.grayText, + ) + } - FavoriteAlgoFeedList(listState, accountViewModel, nav) + FavoriteAlgoFeedList(favorites, listState, accountViewModel, nav) } } } @Composable -private fun FavoriteAlgoFeedList( +private fun ColumnScope.FavoriteAlgoFeedList( + favorites: List, listState: LazyListState, accountViewModel: AccountViewModel, nav: INav, ) { - val favorites by accountViewModel.account.favoriteAlgoFeedsList.flowNotes - .collectAsStateWithLifecycle() - if (favorites.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize().padding(24.dp), - contentAlignment = Alignment.Center, + Column( + modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp, vertical = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { + Spacer(modifier = Modifier.weight(1f)) Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp), @@ -156,22 +166,21 @@ private fun FavoriteAlgoFeedList( style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, ) - Text( - text = stringRes(R.string.favorite_dvms_empty_subtitle), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.grayText, - modifier = Modifier.widthIn(max = 280.dp), + FavoriteAlgoFeedsEmptySteps( + ctaLabel = stringRes(R.string.favorite_dvms_empty_cta), + modifier = Modifier.widthIn(max = 320.dp), ) - Button(onClick = { nav.nav(Route.Discover(initialTab = DiscoverTab.ALGOS)) }) { - Text(text = stringRes(R.string.favorite_dvms_empty_cta)) - } + } + Spacer(modifier = Modifier.weight(1f)) + Button(onClick = { nav.nav(Route.Discover(initialTab = DiscoverTab.ALGOS)) }) { + Text(text = stringRes(R.string.favorite_dvms_empty_cta)) } } return } LazyColumn( + modifier = Modifier.weight(1f), state = listState, verticalArrangement = Arrangement.spacedBy(4.dp), contentPadding = FeedPadding, @@ -188,6 +197,14 @@ private fun FavoriteAlgoFeedList( ) } } + Box( + modifier = Modifier.fillMaxWidth().padding(vertical = 32.dp), + contentAlignment = Alignment.Center, + ) { + Button(onClick = { nav.nav(Route.Discover(initialTab = DiscoverTab.ALGOS)) }) { + Text(text = stringRes(R.string.favorite_dvms_add_more)) + } + } } @OptIn(ExperimentalFoundationApi::class) @@ -267,3 +284,75 @@ private fun FavoriteAlgoFeedRow( } } } + +@Composable +private fun FavoriteAlgoFeedsEmptySteps( + ctaLabel: String, + modifier: Modifier = Modifier, +) { + val starInlineId = "star" + val step2Template = stringRes(R.string.favorite_dvms_empty_step2) + val step2Parts = step2Template.split("%1\$s", limit = 2) + + val step2Text = + buildAnnotatedString { + append(step2Parts.first()) + appendInlineContent(starInlineId, "[star]") + if (step2Parts.size > 1) append(step2Parts[1]) + } + + val starInlineContent = + mapOf( + starInlineId to + InlineTextContent( + Placeholder( + width = 18.sp, + height = 18.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ), + ) { + Icon( + symbol = MaterialSymbols.StarBorder, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + }, + ) + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + NumberedStep(number = "1.") { + Text( + text = stringRes(R.string.favorite_dvms_empty_step1, ctaLabel), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.grayText, + ) + } + NumberedStep(number = "2.") { + Text( + text = step2Text, + inlineContent = starInlineContent, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.grayText, + ) + } + } +} + +@Composable +private fun NumberedStep( + number: String, + content: @Composable () -> Unit, +) { + Row(verticalAlignment = Alignment.Top) { + Text( + text = number, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold, + modifier = Modifier.padding(end = 8.dp), + ) + content() + } +} diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 5e87fff51..dc4cdb6c0 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2181,7 +2181,10 @@ Feed algorithms you star appear here and as filter chips on the Home feed. Pin your favorite algorithms Tap the button below to browse algorithms and star the ones you want to keep here. + Tap \"%1$s\" below to browse algorithms. + Tap the %1$s next to a feed to keep it here. Add some feeds + Add more… Asking %1$s for a feed… Asking your favorite feed algorithms for feeds… Processing your feed… From 7d4eaa4faa6559dadbf899abe18465a677ee20dc Mon Sep 17 00:00:00 2001 From: davotoula Date: Thu, 14 May 2026 22:27:26 +0200 Subject: [PATCH 2/2] Code review: - hoist star inline id, fix TalkBack alt text, add translator note - split empty state, dedupe CTA, remember inline content --- .../favorites/FavoriteAlgoFeedsListScreen.kt | 156 ++++++++++-------- amethyst/src/main/res/values/strings.xml | 2 +- 2 files changed, 91 insertions(+), 67 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/favorites/FavoriteAlgoFeedsListScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/favorites/FavoriteAlgoFeedsListScreen.kt index 6b5837cd8..fcd4be77b 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/favorites/FavoriteAlgoFeedsListScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/dvms/favorites/FavoriteAlgoFeedsListScreen.kt @@ -46,11 +46,13 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment.Companion.BottomStart import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.buildAnnotatedString @@ -82,6 +84,8 @@ import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.grayText import kotlinx.coroutines.launch +private const val STAR_INLINE_ID = "star" + @Composable fun FavoriteAlgoFeedsListScreen( accountViewModel: AccountViewModel, @@ -130,13 +134,50 @@ fun FavoriteAlgoFeedsListScreen( style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.grayText, ) + FavoriteAlgoFeedsEmptyState(nav) + } else { + FavoriteAlgoFeedList(favorites, listState, accountViewModel, nav) } - - FavoriteAlgoFeedList(favorites, listState, accountViewModel, nav) } } } +@Composable +private fun FavoriteAlgoFeedsEmptyState(nav: INav) { + Column( + modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp, vertical = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.weight(1f)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Icon( + symbol = MaterialSymbols.AutoAwesome, + contentDescription = null, + modifier = Modifier.size(72.dp), + tint = MaterialTheme.colorScheme.primary, + ) + Text( + text = stringRes(R.string.favorite_dvms_empty_headline), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + FavoriteAlgoFeedsEmptySteps( + ctaLabel = stringRes(R.string.favorite_dvms_empty_cta), + modifier = Modifier.widthIn(max = 320.dp), + ) + } + Spacer(modifier = Modifier.weight(1f)) + BrowseAlgosButton( + label = stringRes(R.string.favorite_dvms_empty_cta), + nav = nav, + ) + } +} + @Composable private fun ColumnScope.FavoriteAlgoFeedList( favorites: List, @@ -144,41 +185,6 @@ private fun ColumnScope.FavoriteAlgoFeedList( accountViewModel: AccountViewModel, nav: INav, ) { - if (favorites.isEmpty()) { - Column( - modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp, vertical = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Spacer(modifier = Modifier.weight(1f)) - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - Icon( - symbol = MaterialSymbols.AutoAwesome, - contentDescription = null, - modifier = Modifier.size(72.dp), - tint = MaterialTheme.colorScheme.primary, - ) - Text( - text = stringRes(R.string.favorite_dvms_empty_headline), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - ) - FavoriteAlgoFeedsEmptySteps( - ctaLabel = stringRes(R.string.favorite_dvms_empty_cta), - modifier = Modifier.widthIn(max = 320.dp), - ) - } - Spacer(modifier = Modifier.weight(1f)) - Button(onClick = { nav.nav(Route.Discover(initialTab = DiscoverTab.ALGOS)) }) { - Text(text = stringRes(R.string.favorite_dvms_empty_cta)) - } - } - return - } - LazyColumn( modifier = Modifier.weight(1f), state = listState, @@ -197,13 +203,24 @@ private fun ColumnScope.FavoriteAlgoFeedList( ) } } - Box( - modifier = Modifier.fillMaxWidth().padding(vertical = 32.dp), - contentAlignment = Alignment.Center, + BrowseAlgosButton( + label = stringRes(R.string.favorite_dvms_add_more), + nav = nav, + modifier = Modifier.align(Alignment.CenterHorizontally).padding(vertical = 32.dp), + ) +} + +@Composable +private fun BrowseAlgosButton( + label: String, + nav: INav, + modifier: Modifier = Modifier, +) { + Button( + onClick = { nav.nav(Route.Discover(initialTab = DiscoverTab.ALGOS)) }, + modifier = modifier, ) { - Button(onClick = { nav.nav(Route.Discover(initialTab = DiscoverTab.ALGOS)) }) { - Text(text = stringRes(R.string.favorite_dvms_add_more)) - } + Text(text = label) } } @@ -290,34 +307,38 @@ private fun FavoriteAlgoFeedsEmptySteps( ctaLabel: String, modifier: Modifier = Modifier, ) { - val starInlineId = "star" val step2Template = stringRes(R.string.favorite_dvms_empty_step2) - val step2Parts = step2Template.split("%1\$s", limit = 2) + val primaryColor = MaterialTheme.colorScheme.primary val step2Text = - buildAnnotatedString { - append(step2Parts.first()) - appendInlineContent(starInlineId, "[star]") - if (step2Parts.size > 1) append(step2Parts[1]) + remember(step2Template) { + val parts = step2Template.split("%1\$s", limit = 2) + buildAnnotatedString { + append(parts.first()) + appendInlineContent(STAR_INLINE_ID, "star") + if (parts.size > 1) append(parts[1]) + } } val starInlineContent = - mapOf( - starInlineId to - InlineTextContent( - Placeholder( - width = 18.sp, - height = 18.sp, - placeholderVerticalAlign = PlaceholderVerticalAlign.Center, - ), - ) { - Icon( - symbol = MaterialSymbols.StarBorder, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - ) - }, - ) + remember(primaryColor) { + mapOf( + STAR_INLINE_ID to + InlineTextContent( + Placeholder( + width = 18.sp, + height = 18.sp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ), + ) { + Icon( + symbol = MaterialSymbols.StarBorder, + contentDescription = null, + tint = primaryColor, + ) + }, + ) + } Column( modifier = modifier, @@ -346,7 +367,10 @@ private fun NumberedStep( number: String, content: @Composable () -> Unit, ) { - Row(verticalAlignment = Alignment.Top) { + Row( + modifier = Modifier.semantics(mergeDescendants = true) {}, + verticalAlignment = Alignment.Top, + ) { Text( text = number, style = MaterialTheme.typography.bodyMedium, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index dc4cdb6c0..a17c82a1f 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2180,8 +2180,8 @@ Favorite Feed Algorithms Feed algorithms you star appear here and as filter chips on the Home feed. Pin your favorite algorithms - Tap the button below to browse algorithms and star the ones you want to keep here. Tap \"%1$s\" below to browse algorithms. + Tap the %1$s next to a feed to keep it here. Add some feeds Add more…