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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LyxWy2k3AT1LiZSvMsiDx
This commit is contained in:
Claude
2026-06-24 16:04:46 +00:00
parent 4f6a21c55d
commit 2df0c4d6bb
3 changed files with 67 additions and 10 deletions
@@ -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,
)
}
},
@@ -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<FavoriteApp.WebUrl>().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<FavoriteApp>,
history: List<BrowserHistoryEntry>,
iconKeys: Set<String>,
favoriteUrls: Set<String>,
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<String>,
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()
},
)
}
}
}
}
+2
View File
@@ -675,6 +675,8 @@
<string name="browser_go">Open</string>
<string name="browser_clear">Clear</string>
<string name="browser_favorites">Favorites</string>
<string name="browser_recent_options">Options</string>
<string name="browser_recent_remove">Remove from history</string>
<string name="favorite_apps">Favorite apps</string>
<string name="favorite_apps_empty">No favorite apps yet. Open a web client or nsite and tap the star to pin it here.</string>
<string name="favorite_app_add">Add to favorites</string>