fix(client/android): a pinned console tile is a shortcut, not a second host

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.
This commit is contained in:
2026-07-29 12:17:18 +02:00
parent 42bd5be941
commit f6f991648d
3 changed files with 42 additions and 7 deletions
@@ -275,7 +275,7 @@ fun ConnectScreen(
var editTarget by remember { mutableStateOf<KnownHost?>(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<KnownHost?>(null) }
var optionsTarget by remember { mutableStateOf<HostCardEntry?>(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,
)
}
@@ -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."
},
)
}
}
@@ -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
}
/**