mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
feat: move favorite to pull-down sheet, remove from OmniBar
Remove the star icon from the browser URL bar (OmniBar) and add a favorite toggle row to both pull-down sheet surfaces instead: - Compose TopControlSheet (embedded web tabs and napplets): shows filled/outline star with "Add to favorites" / "Remove from favorites" sourced from FavoriteAppsRegistry; isFavorite state flows reactively through EmbeddedTabChrome so the label updates without reopening. - Native NappletControlSheet (full-screen NappletBrowserActivity): same toggle backed by a new MSG_TOGGLE_WEB_FAVORITE IPC message handled in NappletBrokerService; initial state is passed via intent so the star opens in the correct filled/outline state for the launch URL. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SBpBE7bQJ2JRni6sYG8UDo
This commit is contained in:
@@ -77,8 +77,9 @@ object FavoriteAppLauncher {
|
||||
) {
|
||||
val proxyPort = Amethyst.instance.torManager.activePortOrNull.value ?: -1
|
||||
val useTor = proxyPort > 0 && (preferTor || WebUrlNetworkRegistry.useTor(url))
|
||||
val isFavorite = FavoriteAppsRegistry.isFavorite("url:$url")
|
||||
val intent =
|
||||
NappletBrowserActivity.intent(context, url, proxyPort, useTor).apply {
|
||||
NappletBrowserActivity.intent(context, url, proxyPort, useTor, isFavorite = isFavorite).apply {
|
||||
if (context !is Activity) addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
|
||||
@@ -32,6 +32,7 @@ import android.os.RemoteException
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletBroker
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletCapability
|
||||
import com.vitorpamplona.amethyst.commons.napplet.NappletIdentity
|
||||
@@ -41,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletProtocolJson
|
||||
import com.vitorpamplona.amethyst.commons.napplet.protocol.NappletResponse
|
||||
import com.vitorpamplona.amethyst.favorites.BrowserHistoryRegistry
|
||||
import com.vitorpamplona.amethyst.favorites.BrowserIconRegistry
|
||||
import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.napplet.gateways.AccountNappletGateways
|
||||
import com.vitorpamplona.amethyst.napplethost.NappletIpc
|
||||
@@ -175,6 +177,21 @@ class NappletBrokerService : Service() {
|
||||
return true
|
||||
}
|
||||
|
||||
// The direct-WebView browser requests a favorite toggle for the current URL (main process only).
|
||||
if (msg.what == NappletIpc.MSG_TOGGLE_WEB_FAVORITE) {
|
||||
val data = msg.data ?: return true
|
||||
val url = data.getString(NappletIpc.KEY_FAVORITE_URL)?.takeIf { it.isNotBlank() } ?: return true
|
||||
val label = data.getString(NappletIpc.KEY_FAVORITE_LABEL).orEmpty().ifBlank { url }
|
||||
FavoriteAppsRegistry.init(applicationContext)
|
||||
val id = "url:$url"
|
||||
if (FavoriteAppsRegistry.isFavorite(id)) {
|
||||
FavoriteAppsRegistry.remove(id)
|
||||
} else {
|
||||
FavoriteAppsRegistry.add(FavoriteApp.WebUrl(url, label, System.currentTimeMillis()))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// The direct-WebView browser relays its per-host Tor choice; persist it (main process only).
|
||||
if (msg.what == NappletIpc.MSG_SET_WEB_TOR) {
|
||||
val data = msg.data ?: return true
|
||||
|
||||
-10
@@ -195,12 +195,6 @@ private fun BrowserLauncher(
|
||||
onValueChange = ::onValueChange,
|
||||
onClear = { field = TextFieldValue("") },
|
||||
onOpen = { open(field.text) },
|
||||
onFavorite = {
|
||||
val url = OmniboxInput.resolve(field.text)?.url ?: return@OmniBar
|
||||
FavoriteAppsRegistry.add(
|
||||
FavoriteApp.WebUrl(url = url, label = hostOf(url), addedAt = System.currentTimeMillis()),
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
@@ -264,7 +258,6 @@ private fun OmniBar(
|
||||
onValueChange: (TextFieldValue) -> Unit,
|
||||
onClear: () -> Unit,
|
||||
onOpen: () -> Unit,
|
||||
onFavorite: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier =
|
||||
@@ -309,9 +302,6 @@ private fun OmniBar(
|
||||
),
|
||||
)
|
||||
if (field.text.isNotBlank()) {
|
||||
IconButton(onClick = onFavorite) {
|
||||
Icon(MaterialSymbols.StarBorder, contentDescription = stringResource(R.string.favorite_app_add))
|
||||
}
|
||||
IconButton(onClick = onOpen) {
|
||||
Icon(MaterialSymbols.AutoMirrored.ArrowForward, contentDescription = stringResource(R.string.browser_go))
|
||||
}
|
||||
|
||||
+16
-1
@@ -44,9 +44,12 @@ import androidx.compose.ui.layout.boundsInWindow
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
|
||||
import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher
|
||||
import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry
|
||||
import com.vitorpamplona.amethyst.napplet.WebUrlNetworkRegistry
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.favoriteIds
|
||||
@@ -104,6 +107,9 @@ private fun EmbeddedFavoriteTab(
|
||||
// can opt one out and it must stick). Only meaningful when Tor is actually available.
|
||||
var torOn by remember { mutableStateOf(proxyAvailable && WebUrlNetworkRegistry.useTor(url)) }
|
||||
|
||||
val apps by FavoriteAppsRegistry.favorites.collectAsStateWithLifecycle()
|
||||
val isFavorite = remember(apps, currentUrl) { apps.any { it is FavoriteApp.WebUrl && it.url == currentUrl } }
|
||||
|
||||
val backgroundColor = MaterialTheme.colorScheme.background.toArgb()
|
||||
|
||||
val controller =
|
||||
@@ -121,7 +127,7 @@ private fun EmbeddedFavoriteTab(
|
||||
|
||||
// Rebuilt only when a displayed value changes, so the tab layer isn't recomposed every frame.
|
||||
val chrome =
|
||||
remember(currentUrl, torOn, proxyAvailable) {
|
||||
remember(currentUrl, torOn, proxyAvailable, isFavorite) {
|
||||
EmbeddedTabChrome(
|
||||
title = hostLabel(currentUrl),
|
||||
isSandbox = false,
|
||||
@@ -133,6 +139,15 @@ private fun EmbeddedFavoriteTab(
|
||||
controller.setTor(torOn)
|
||||
WebUrlNetworkRegistry.set(url, torOn)
|
||||
},
|
||||
isFavorite = isFavorite,
|
||||
onFavorite = {
|
||||
val favId = "url:$currentUrl"
|
||||
if (FavoriteAppsRegistry.isFavorite(favId)) {
|
||||
FavoriteAppsRegistry.remove(favId)
|
||||
} else {
|
||||
FavoriteAppsRegistry.add(FavoriteApp.WebUrl(currentUrl, hostLabel(currentUrl), System.currentTimeMillis()))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
// Publish the top-sheet controls to the tab layer (which draws them over the z-below surface). In a
|
||||
|
||||
+4
@@ -37,4 +37,8 @@ data class EmbeddedTabChrome(
|
||||
val onToggleTor: () -> Unit = {},
|
||||
/** The "what it can access" sheet, for sandboxed napplets/nsites; null for a plain web client. */
|
||||
val onInfo: (() -> Unit)? = null,
|
||||
/** Whether the current URL/app is already saved as a favorite. */
|
||||
val isFavorite: Boolean = false,
|
||||
/** Toggles the current site/app in the favorites registry; null when not applicable. */
|
||||
val onFavorite: (() -> Unit)? = null,
|
||||
)
|
||||
|
||||
+9
@@ -131,6 +131,15 @@ fun TopControlSheet(
|
||||
onExpandedChange(false)
|
||||
chrome.onOpenFull()
|
||||
}
|
||||
chrome.onFavorite?.let { toggleFavorite ->
|
||||
SheetItem(
|
||||
if (chrome.isFavorite) MaterialSymbols.Star else MaterialSymbols.StarBorder,
|
||||
stringResource(if (chrome.isFavorite) R.string.favorite_app_remove else R.string.favorite_app_add),
|
||||
) {
|
||||
onExpandedChange(false)
|
||||
toggleFavorite()
|
||||
}
|
||||
}
|
||||
onConsole?.let { showConsole ->
|
||||
SheetItem(
|
||||
MaterialSymbols.Code,
|
||||
|
||||
+16
-2
@@ -53,9 +53,11 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteApp
|
||||
import com.vitorpamplona.amethyst.favorites.FavoriteAppLauncher
|
||||
import com.vitorpamplona.amethyst.favorites.FavoriteAppsRegistry
|
||||
import com.vitorpamplona.amethyst.napplethost.NappletEmbedContract
|
||||
import com.vitorpamplona.amethyst.napplethost.NappletHostContract
|
||||
import com.vitorpamplona.amethyst.ui.navigation.bottombars.AppBottomBar
|
||||
@@ -125,6 +127,9 @@ private fun EmbeddedNappletTab(
|
||||
var canGoBack by remember { mutableStateOf(false) }
|
||||
var showAccess by remember { mutableStateOf(false) }
|
||||
|
||||
val apps by FavoriteAppsRegistry.favorites.collectAsStateWithLifecycle()
|
||||
val isFavorite = remember(apps, coordinate) { apps.any { it.id == "nostr:$coordinate" } }
|
||||
|
||||
val controller =
|
||||
remember(id) {
|
||||
EmbeddedTabFactory.acquireNapplet(context, coordinate, params, backgroundColor)
|
||||
@@ -138,15 +143,24 @@ private fun EmbeddedNappletTab(
|
||||
}
|
||||
}
|
||||
|
||||
// Stable per app (title/coordinate don't change), so the tab layer isn't recomposed every frame.
|
||||
// Stable per app (title/coordinate/isFavorite don't change often), so the tab layer isn't recomposed every frame.
|
||||
val chrome =
|
||||
remember(title, coordinate) {
|
||||
remember(title, coordinate, isFavorite) {
|
||||
EmbeddedTabChrome(
|
||||
title = title.ifBlank { coordinate },
|
||||
isSandbox = true,
|
||||
onReload = { controller.reload() },
|
||||
onOpenFull = { FavoriteAppLauncher.launch(context, FavoriteApp.NostrApp(coordinate, title, System.currentTimeMillis())) },
|
||||
onInfo = { showAccess = true },
|
||||
isFavorite = isFavorite,
|
||||
onFavorite = {
|
||||
val favId = "nostr:$coordinate"
|
||||
if (FavoriteAppsRegistry.isFavorite(favId)) {
|
||||
FavoriteAppsRegistry.remove(favId)
|
||||
} else {
|
||||
FavoriteAppsRegistry.add(FavoriteApp.NostrApp(coordinate, title, System.currentTimeMillis()))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
// Publish the top-sheet controls to the tab layer (drawn over the z-below surface). In a SideEffect
|
||||
|
||||
+23
@@ -590,8 +590,28 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
liveUrl = startUrl,
|
||||
onNavigate = { loadAddress(it) },
|
||||
onConsole = { consolePanel?.toggle() },
|
||||
isFavoriteInitially = intent.getBooleanExtra(EXTRA_IS_FAVORITE, false),
|
||||
onFavoriteToggle = { url, _ -> sendFavoriteToggle(url) },
|
||||
).also { controlSheet = it }
|
||||
|
||||
private fun sendFavoriteToggle(url: String) {
|
||||
val host =
|
||||
runCatching {
|
||||
android.net.Uri
|
||||
.parse(url)
|
||||
.host
|
||||
}.getOrNull()?.takeIf { it.isNotBlank() } ?: url
|
||||
val msg =
|
||||
Message.obtain(null, NappletIpc.MSG_TOGGLE_WEB_FAVORITE).apply {
|
||||
data =
|
||||
Bundle().apply {
|
||||
putString(NappletIpc.KEY_FAVORITE_URL, url)
|
||||
putString(NappletIpc.KEY_FAVORITE_LABEL, host)
|
||||
}
|
||||
}
|
||||
if (brokerMessenger != null) sendToBroker(msg) else pendingBrokerRequests.add(msg)
|
||||
}
|
||||
|
||||
private fun buildConsolePanel(): View =
|
||||
NappletConsolePanel(this).also {
|
||||
it.onClearCallback = { controlSheet?.updateConsoleCount(0) }
|
||||
@@ -639,6 +659,7 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
private const val EXTRA_PROXY_PORT = "proxyPort"
|
||||
private const val EXTRA_USE_TOR = "useTor"
|
||||
private const val EXTRA_TITLE = "title"
|
||||
private const val EXTRA_IS_FAVORITE = "isFavorite"
|
||||
|
||||
fun intent(
|
||||
context: Context,
|
||||
@@ -646,6 +667,7 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
proxyPort: Int,
|
||||
useTor: Boolean,
|
||||
title: String = "",
|
||||
isFavorite: Boolean = false,
|
||||
): Intent =
|
||||
Intent()
|
||||
.setClassName(context, "com.vitorpamplona.amethyst.napplethost.NappletBrowserActivity")
|
||||
@@ -653,6 +675,7 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
.putExtra(EXTRA_PROXY_PORT, proxyPort)
|
||||
.putExtra(EXTRA_USE_TOR, useTor)
|
||||
.putExtra(EXTRA_TITLE, title)
|
||||
.putExtra(EXTRA_IS_FAVORITE, isFavorite)
|
||||
// Distinct task identity per URL for documentLaunchMode=intoExisting.
|
||||
.setData(Uri.parse(url))
|
||||
}
|
||||
|
||||
+47
@@ -67,6 +67,9 @@ class NappletControlSheet(
|
||||
// When non-null, a "Console" row is added to the pull-down sheet. The callback toggles the
|
||||
// browser's console log panel; the count label is updated via [updateConsoleCount].
|
||||
private val onConsole: (() -> Unit)? = null,
|
||||
// When non-null, a favorite toggle row is shown; called with the current URL and new isFavorite state.
|
||||
isFavoriteInitially: Boolean = false,
|
||||
private val onFavoriteToggle: ((url: String, isFavorite: Boolean) -> Unit)? = null,
|
||||
) : LinearLayout(context) {
|
||||
private val onSurface = resolveThemeColor(android.R.attr.textColorPrimary)
|
||||
private val dimmed = resolveThemeColor(android.R.attr.textColorSecondary)
|
||||
@@ -75,6 +78,7 @@ class NappletControlSheet(
|
||||
private var expanded = false
|
||||
private var torOn = torInitiallyOn
|
||||
private var currentUrl = liveUrl
|
||||
private var isFavorite = isFavoriteInitially
|
||||
|
||||
private val panel: LinearLayout
|
||||
private var torLabel: TextView? = null
|
||||
@@ -82,6 +86,7 @@ class NappletControlSheet(
|
||||
private var addressField: EditText? = null
|
||||
private var securityGlyph: TextView? = null
|
||||
private var consoleLabel: TextView? = null
|
||||
private var favoriteLabel: TextView? = null
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
@@ -156,6 +161,35 @@ class NappletControlSheet(
|
||||
},
|
||||
)
|
||||
}
|
||||
onFavoriteToggle?.let {
|
||||
val label =
|
||||
TextView(context).apply {
|
||||
text = context.getString(if (isFavorite) R.string.browser_favorite_remove else R.string.browser_favorite_add)
|
||||
setTextColor(onSurface)
|
||||
textSize = 15f
|
||||
setPadding(dp(8), 0, 0, 0)
|
||||
}
|
||||
favoriteLabel = label
|
||||
addView(
|
||||
LinearLayout(context).apply {
|
||||
orientation = HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
setPadding(dp(8), dp(10), dp(8), dp(10))
|
||||
isClickable = true
|
||||
setOnClickListener { toggleFavorite() }
|
||||
addView(
|
||||
TextView(context).apply {
|
||||
text = "★"
|
||||
setTextColor(dimmed)
|
||||
textSize = 18f
|
||||
width = dp(28)
|
||||
gravity = Gravity.CENTER
|
||||
},
|
||||
)
|
||||
addView(label)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun titleRow(): View =
|
||||
@@ -254,6 +288,19 @@ class NappletControlSheet(
|
||||
// Don't fight the user while they're editing the field.
|
||||
addressField?.takeIf { !it.hasFocus() }?.setText(url)
|
||||
securityGlyph?.text = securityGlyphFor(url)
|
||||
// Reset favorite state for the new URL (we don't know if it's a favorite without a round-trip).
|
||||
if (onFavoriteToggle != null) {
|
||||
isFavorite = false
|
||||
favoriteLabel?.text = context.getString(R.string.browser_favorite_add)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleFavorite() {
|
||||
val url = currentUrl?.takeIf { it.isNotBlank() } ?: return
|
||||
isFavorite = !isFavorite
|
||||
favoriteLabel?.text = context.getString(if (isFavorite) R.string.browser_favorite_remove else R.string.browser_favorite_add)
|
||||
collapse()
|
||||
onFavoriteToggle?.invoke(url, isFavorite)
|
||||
}
|
||||
|
||||
private fun securityGlyphFor(url: String): String =
|
||||
|
||||
@@ -89,6 +89,14 @@ object NappletIpc {
|
||||
*/
|
||||
const val MSG_RECORD_ICON = 10
|
||||
|
||||
/**
|
||||
* Host → broker (browser mode): toggle a URL in the main-process favorites registry. Carries
|
||||
* [KEY_FAVORITE_URL] and [KEY_FAVORITE_LABEL]. The broker adds the URL if it isn't already
|
||||
* a favorite, or removes it if it is — identical to the in-app star toggle on the home screen.
|
||||
* Fire-and-forget; no reply needed.
|
||||
*/
|
||||
const val MSG_TOGGLE_WEB_FAVORITE = 11
|
||||
|
||||
const val KEY_REQUEST_ID = "requestId"
|
||||
const val KEY_PAYLOAD = "payload"
|
||||
|
||||
@@ -107,6 +115,12 @@ object NappletIpc {
|
||||
/** The bare host (e.g. `example.com`) a browser Tor choice belongs to. */
|
||||
const val KEY_WEB_HOST = "webHost"
|
||||
|
||||
/** The full URL (e.g. `https://example.com`) to toggle as a web favorite. */
|
||||
const val KEY_FAVORITE_URL = "favoriteUrl"
|
||||
|
||||
/** A human-readable label for the favorited URL (typically the host). */
|
||||
const val KEY_FAVORITE_LABEL = "favoriteLabel"
|
||||
|
||||
/** Boolean: this sandbox surface is now foreground (true) or backgrounded (false). */
|
||||
const val KEY_FOREGROUND = "foreground"
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
<string name="napplet_net_tor_label">Loads over Tor</string>
|
||||
<string name="napplet_net_open_label">Loads over the open web</string>
|
||||
|
||||
<!-- Favorite toggle in the pull-down sheet -->
|
||||
<string name="browser_favorite_add">Add to favorites</string>
|
||||
<string name="browser_favorite_remove">Remove from favorites</string>
|
||||
|
||||
<!-- Loading / unavailable screens -->
|
||||
<string name="napplet_unavailable_title">Couldn\'t load “%1$s”</string>
|
||||
<string name="napplet_unavailable_subtitle">The publisher\'s servers may be offline, or you\'re not connected. You can try again.</string>
|
||||
|
||||
Reference in New Issue
Block a user