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