From 2df0c4d6bb8a476d1f089baf106af2ace60c9aa8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Jun 2026 16:04:46 +0000 Subject: [PATCH] feat: favicons in bottom nav + 3-dot menu on recent rows - Pinned web favorites in the bottom navigation now show the captured favicon instead of the generic globe (falling back to the globe until one is captured). Resolved via BrowserIconRegistry, same as the launcher cards. - Each Recent row in the browser home gains a 3-dot overflow menu to add the URL to favorites (or remove it if already favorited) and to remove it from history. Replaces the prior long-press-to-remove with a discoverable menu; the row icon shows a star once the site is favorited. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_017LyxWy2k3AT1LiZSvMsiDx --- .../ui/navigation/bottombars/AppBottomBar.kt | 16 ++++- .../screen/loggedIn/browser/BrowserScreen.kt | 59 ++++++++++++++++--- amethyst/src/main/res/values/strings.xml | 2 + 3 files changed, 67 insertions(+), 10 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt index bb8f07f1c4..9cc9ffee7c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/bottombars/AppBottomBar.kt @@ -42,9 +42,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.vitorpamplona.amethyst.commons.browser.OmniboxInput import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppIcon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon +import com.vitorpamplona.amethyst.favorites.BrowserIconRegistry import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route @@ -109,6 +111,9 @@ private fun RenderBottomMenu( // Index favorites by id so resolving each Favorite entry is a map lookup, not a per-entry scan. val favoritesById = remember(favorites) { favorites.associateBy { it.id } } + // Captured favicons, so a pinned web favorite shows the site's icon instead of the generic globe. + val iconKeys by BrowserIconRegistry.keys.collectAsStateWithLifecycle() + Column( modifier = Modifier @@ -141,7 +146,11 @@ private fun RenderBottomMenu( is FavoriteApp.WebUrl -> Route.FavoriteWebApp(fav.url) is FavoriteApp.NostrApp -> Route.FavoriteNostrApp(fav.coordinate) } - FavoriteNavItem(destination == selectedRoute, fav, destination, nav) + val iconModel = + remember(fav, iconKeys) { + (fav as? FavoriteApp.WebUrl)?.let { OmniboxInput.hostOf(it.url)?.let(BrowserIconRegistry::iconModelFor) } + } + FavoriteNavItem(destination == selectedRoute, fav, iconModel, destination, nav) } } } @@ -153,6 +162,7 @@ private fun RenderBottomMenu( private fun RowScope.FavoriteNavItem( selected: Boolean, fav: FavoriteApp, + iconModel: Any?, destination: Route, nav: (Route) -> Unit, ) { @@ -160,11 +170,13 @@ private fun RowScope.FavoriteNavItem( alwaysShowLabel = false, icon = { Box(Size27Modifier, contentAlignment = Alignment.Center) { - // The app's own icon (nsite/napplet manifest icon) when it has one, else a type glyph. + // A web favorite's captured favicon (else the globe); an nsite/napplet's manifest icon (else + // the grid glyph). FavoriteAppIcon( app = fav, tint = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface65, modifier = Size25Modifier, + iconModel = iconModel, ) } }, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt index b071ff0413..ba72b350ee 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/browser/BrowserScreen.kt @@ -21,10 +21,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.browser import android.os.Build -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -44,6 +42,8 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -229,17 +229,30 @@ private fun BrowserLauncher( color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - else -> + else -> { + val favoriteUrls = remember(apps) { apps.filterIsInstance().mapTo(HashSet()) { it.url } } BrowserHome( apps = apps, history = history, iconKeys = iconKeys, + favoriteUrls = favoriteUrls, onOpenApp = { FavoriteAppLauncher.launch(context, it) }, onRemoveApp = { FavoriteAppsRegistry.remove(it.id) }, onOpenUrl = { open(it) }, + onToggleRecentFavorite = { entry -> + val id = "url:" + entry.url + if (FavoriteAppsRegistry.isFavorite(id)) { + FavoriteAppsRegistry.remove(id) + } else { + FavoriteAppsRegistry.add( + FavoriteApp.WebUrl(entry.url, entry.title.ifBlank { entry.host }, System.currentTimeMillis()), + ) + } + }, onRemoveRecent = { BrowserHistoryRegistry.remove(it) }, modifier = contentModifier, ) + } } } } @@ -368,15 +381,16 @@ private fun SuggestionRow( } /** The idle body: favorites grid on top, then recent visits — in one grid so they scroll together. */ -@OptIn(ExperimentalFoundationApi::class) @Composable private fun BrowserHome( apps: List, history: List, iconKeys: Set, + favoriteUrls: Set, onOpenApp: (FavoriteApp) -> Unit, onRemoveApp: (FavoriteApp) -> Unit, onOpenUrl: (String) -> Unit, + onToggleRecentFavorite: (BrowserHistoryEntry) -> Unit, onRemoveRecent: (String) -> Unit, modifier: Modifier = Modifier, ) { @@ -398,7 +412,9 @@ private fun BrowserHome( RecentRow( entry = entry, iconKeys = iconKeys, + isFavorited = entry.url in favoriteUrls, onClick = { onOpenUrl(entry.url) }, + onToggleFavorite = { onToggleRecentFavorite(entry) }, onRemove = { onRemoveRecent(entry.url) }, ) } @@ -406,24 +422,26 @@ private fun BrowserHome( } } -@OptIn(ExperimentalFoundationApi::class) @Composable private fun RecentRow( entry: BrowserHistoryEntry, iconKeys: Set, + isFavorited: Boolean, onClick: () -> Unit, + onToggleFavorite: () -> Unit, onRemove: () -> Unit, ) { + var menuOpen by remember { mutableStateOf(false) } Row( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) - .combinedClickable(onClick = onClick, onLongClick = onRemove) - .padding(horizontal = 8.dp, vertical = 10.dp), + .clickable(onClick = onClick) + .padding(start = 8.dp, top = 4.dp, bottom = 4.dp), verticalAlignment = Alignment.CenterVertically, ) { - SiteIcon(entry.host, isFavorite = false, iconKeys = iconKeys, modifier = Modifier.size(24.dp)) + SiteIcon(entry.host, isFavorite = isFavorited, iconKeys = iconKeys, modifier = Modifier.size(24.dp)) Spacer(Modifier.width(16.dp)) Column(Modifier.weight(1f)) { Text( @@ -440,6 +458,31 @@ private fun RecentRow( overflow = TextOverflow.Ellipsis, ) } + Box { + IconButton(onClick = { menuOpen = true }) { + Icon(MaterialSymbols.MoreVert, contentDescription = stringResource(R.string.browser_recent_options)) + } + DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) { + DropdownMenuItem( + text = { Text(stringResource(if (isFavorited) R.string.favorite_app_remove else R.string.favorite_app_add)) }, + leadingIcon = { + Icon(if (isFavorited) MaterialSymbols.Star else MaterialSymbols.StarBorder, contentDescription = null) + }, + onClick = { + menuOpen = false + onToggleFavorite() + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.browser_recent_remove)) }, + leadingIcon = { Icon(MaterialSymbols.Delete, contentDescription = null) }, + onClick = { + menuOpen = false + onRemove() + }, + ) + } + } } } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index c098ea201f..413b463983 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -675,6 +675,8 @@ Open Clear Favorites + Options + Remove from history Favorite apps No favorite apps yet. Open a web client or nsite and tap the star to pin it here. Add to favorites