mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-10 00:16:59 +00:00
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:
+14
-2
@@ -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,
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
+51
-8
@@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user