From f6f991648db73dc191508ca4a7ba6095525c2ef0 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 29 Jul 2026 08:57:21 +0200 Subject: [PATCH] fix(client/android): a pinned console tile is a shortcut, not a second host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The touch grid withholds Edit / Forget / Wake from a pinned card on purpose — a pin is a shortcut to one host+profile combination, and offering the host's destructive actions on it blurs exactly that. The console carousel didn't: its pinned tiles carried the host record, so Up on one opened the full host options (including Forget), and Y opened the host's library. They now carry which profile they pin, so the options dialog offers the one action a pin has — Unpin — and says what unpinning does and doesn't touch. --- .../kotlin/io/unom/punktfunk/ConnectScreen.kt | 18 +++++++++++---- .../io/unom/punktfunk/GamepadDialogs.kt | 23 +++++++++++++++++-- .../kotlin/io/unom/punktfunk/GamepadHome.kt | 8 ++++++- 3 files changed, 42 insertions(+), 7 deletions(-) diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/ConnectScreen.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/ConnectScreen.kt index aac7f42c..b6a2d8ef 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/ConnectScreen.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/ConnectScreen.kt @@ -275,7 +275,7 @@ fun ConnectScreen( var editTarget by remember { mutableStateOf(null) } // A saved host whose console options menu (Wake / Edit / Forget) is open — reached with Up on the // carousel (the console counterpart of the touch host card's overflow menu). - var optionsTarget by remember { mutableStateOf(null) } + var optionsTarget by remember { mutableStateOf(null) } // Discovered hosts not already saved — a saved host (paired or TOFU) belongs in "Saved hosts", // not also in "Discovered", so we hide the overlap (matched by fingerprint when both carry it, so @@ -718,6 +718,7 @@ fun ConnectScreen( online = kh.isOnline(discovered, reachable), paired = kh.paired, knownHost = kh, + pinnedProfileId = p.id, activate = { connect(kh.address, kh.port, oneOffProfile = p.id) }, ), ) @@ -758,7 +759,11 @@ fun ConnectScreen( onActivate = { it.activate() }, onOpenLibrary = { it.knownHost?.let(onOpenLibrary) }, onOpenSettings = onOpenSettings, - onOptions = { it.knownHost?.let { kh -> optionsTarget = kh } }, + onOptions = { tile -> + tile.knownHost?.let { kh -> + optionsTarget = HostCardEntry(kh, tile.pinnedProfileId?.let(profileStore::byId)) + } + }, ) } else { Box(Modifier.fillMaxSize()) { @@ -1028,7 +1033,9 @@ fun ConnectScreen( } // Console host options (Up on a saved carousel tile): Wake / Edit / Forget. - optionsTarget?.let { kh -> + optionsTarget?.let { entry -> + val kh = entry.host + val pin = entry.pin val offline = !kh.isOnline(discovered, reachable) GamepadHostOptionsDialog( hostName = kh.name, @@ -1048,7 +1055,7 @@ fun ConnectScreen( }, // A saved host always has a library (it's a knownHost) → offer it when the setting's on, // so a TV remote reaches the library here instead of via the Y face button. - onLibrary = if (settings.libraryEnabled) { + onLibrary = if (settings.libraryEnabled && pin == null) { { optionsTarget = null; onOpenLibrary(kh) } } else { null @@ -1060,6 +1067,9 @@ fun ConnectScreen( optionsTarget = null }, onDismiss = { optionsTarget = null }, + // A pin's only action: unpinning touches neither the host nor the profile. + onUnpin = pin?.let { p -> { togglePin(kh, p); optionsTarget = null } }, + profileName = pin?.name, ) } diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadDialogs.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadDialogs.kt index 2ab5b347..42b66755 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadDialogs.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadDialogs.kt @@ -213,11 +213,23 @@ fun GamepadHostOptionsDialog( onEdit: () -> Unit, onForget: () -> Unit, onDismiss: () -> Unit, + /** + * Non-null when this is a PINNED host+profile tile, whose only action is to unpin. A pin is a + * shortcut, not a second host — offering the host's destructive actions on it would blur + * exactly that, and the touch grid withholds them for the same reason. + */ + onUnpin: (() -> Unit)? = null, + profileName: String? = null, ) { GamepadDialog( - title = hostName, + title = if (profileName != null) "$hostName · $profileName" else hostName, onDismiss = onDismiss, actions = buildList { + if (onUnpin != null) { + add(DialogAction("Unpin card", primary = true, onClick = onUnpin)) + add(DialogAction("Cancel", onClick = onDismiss)) + return@buildList + } if (onLibrary != null) add(DialogAction("Library", primary = true, onClick = onLibrary)) if (canWake) add(DialogAction("Wake host", onClick = onWake)) add(DialogAction("Edit…", primary = onLibrary == null, onClick = onEdit)) @@ -225,7 +237,14 @@ fun GamepadHostOptionsDialog( add(DialogAction("Cancel", onClick = onDismiss)) }, ) { - DialogText("Manage this saved host.") + DialogText( + if (onUnpin != null) { + "This card is a shortcut to this host with one profile. Unpinning it changes " + + "nothing about the host or the profile." + } else { + "Manage this saved host." + }, + ) } } diff --git a/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadHome.kt b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadHome.kt index 15013e6c..d42a4643 100644 --- a/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadHome.kt +++ b/clients/android/app/src/main/kotlin/io/unom/punktfunk/GamepadHome.kt @@ -73,11 +73,17 @@ class HomeTile( val connecting: Boolean = false, val isAdd: Boolean = false, // the trailing Add Host tile (plus icon, not a monogram) val knownHost: KnownHost? = null, // set for saved hosts → enables the library (Y) + /** + * Set when this tile is a PINNED host+profile combination rather than the host's own tile. + * A pin is a shortcut, not a second host: the host-level actions (wake, edit, forget, library) + * belong to the host's own tile, and this one offers only Unpin. + */ + val pinnedProfileId: String? = null, val activate: () -> Unit, ) { // Any SAVED host offers the library (matches Apple) — the fetch itself returns a clear "pair // first" message if the host hasn't authorized this device for its management API. - val hasLibrary: Boolean get() = knownHost != null + val hasLibrary: Boolean get() = knownHost != null && pinnedProfileId == null } /**