ci / bun-nix (pull_request) Successful in 26s
ci / web (pull_request) Successful in 1m4s
ci / docs-site (pull_request) Successful in 1m13s
apple / swift (pull_request) Successful in 1m44s
apple / screenshots (pull_request) Skipped
ci / rust-arm64 (pull_request) Successful in 3m10s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 4m39s
android / android (pull_request) Successful in 7m12s
ci / rust (pull_request) Successful in 7m58s
windows / build (aarch64-pc-windows-msvc) (pull_request) Failing after 11m18s
The console fix before this one closed the leak on one client. The same question
has a different wrong answer on each of the others, so this closes it everywhere:
a pinned host+profile card can be browsed, and every title launched off a pinned
card's shelf streams with that card's profile.
Two shapes of bug, one per client:
**The library was not offered on a pinned card at all** — Apple (both UIs:
`hasLibrary: profile == nil` on the console tile, no menu item on the touch card),
Android (`hasLibrary` required `pinnedProfileId == null`, and the console host
options gated Library on `pin == null`), GTK (the pinned card's menu had Connect,
Copy link, Create shortcut, Unpin — no library) and Windows (pinned tiles had no
menu whatsoever). Each was justified in a comment as "a pin is a shortcut, not a
second host, so the host-level actions stay on the host's tile" — right about
wake, pair, edit and forget, wrong about the library. Browsing is not a property
of the machine: it is this card's connect with a title picked first, which is
exactly what a shortcut is for. So the library joins Connect on every pinned
surface, and the host-level actions stay where they were.
**The launch dropped the profile.** GTK already carried it (its library clones
the card's whole ConnectRequest) and Windows already carried it (its library page
launches through the shared target, which the tile parks). Apple did not: the
library was keyed on a bare `StoredHost`, so `launchTitle` connected with
`.inherit` and the host's binding won even from a pinned card. Android was worse
still, and not only for pins — `LibraryScreen` dialled `connectToHost` with the
RAW settings, so a library launch applied NO profile at all, not even the host's
binding, on every host. Its host list resolves
`settings.effectiveFor(profileStore.resolveFor(kh, oneOff))`; its library resolved
nothing.
So the shelf, not the host, is now the unit both clients navigate by. Apple gets
a `LibraryTarget` (host + `ProfileSelection`) threaded through `libraryTarget`,
the shell's screen enum, both presentations and `SessionModel`; Android passes
the pinned id into `LibraryScreen` and resolves it there through the same
`ProfileStore.resolveFor` rule the connect path uses. Falling out of that:
* a game that exits returns to the shelf it was launched from, pin and all,
rather than to the host's default one — `SessionModel.launchedShelf` on Apple,
`ActiveSession.libraryProfileId` → `LibraryReturn` on Android. Android also
drops a pin that was unpinned while the game ran, rather than reopening a card
that no longer exists.
* Android's stats overlay finally names the profile a library launch used
(`profileName` was never set on that path).
* Apple's `punktfunk://browse/<host>` honours `profile=`, which it parsed and
ignored — and refuses an unknown or ambiguous reference exactly as the connect
route does (§10.6) rather than degrading to the binding.
* every shelf says which one it is, in the card's own `host · profile` shape:
the console's title, GTK's page title, Android's ConsoleHeader, Apple's
navigation title and its console heading.
Verified per platform, none of it on trust:
* console + GTK: fmt, build, clippy `-D warnings` and 85 tests green in the
pf-lxcheck2 container (a Mac `cargo test -p pf-console-ui` compiles nothing).
* Apple: `swift build` green for macOS, iOS and tvOS. Worth all three — the tvOS
pass alone caught `navigationDestination(item:)` needing Hashable, and an
iOS-only screen was confirmed genuinely compiled by planting a type error.
* Android: `compileDebugKotlin` + unit tests, with HomeTilesTest's pin
expectation flipped to match.
* Windows: `cargo check --all-targets` + clippy `-D warnings` on the CI runner,
cold (3m10s) — that client cannot compile on a Mac.
58 lines
2.6 KiB
Swift
58 lines
2.6 KiB
Swift
// The library as one of the gamepad shell's in-place layers (iOS): console chrome — a pinned
|
|
// title and a close ✕ styled like the settings screen's — around the shared LibraryView, whose
|
|
// gamepad branch renders the coverflow. The cover presentation used to get its title and Close
|
|
// from the wrapping NavigationStack's bar; a shell layer has no bar, so this restores both in
|
|
// the console's own grammar. Everything data-shaped (the fetch, the loading/error/empty states,
|
|
// the image session lifecycle) stays LibraryView's.
|
|
|
|
import PunktfunkKit
|
|
import SwiftUI
|
|
#if os(iOS)
|
|
|
|
struct GamepadLibraryScreen: View {
|
|
@Environment(\.gamepadInk) private var ink
|
|
@ObservedObject var store: HostStore
|
|
let target: LibraryTarget
|
|
let onLaunch: (String) -> Void
|
|
let close: () -> Void
|
|
var controllerActive = true
|
|
|
|
/// `.compact` in a landscape phone window — tighter chrome, like every gamepad screen.
|
|
@Environment(\.verticalSizeClass) private var vSizeClass
|
|
/// Resolves a pinned shelf's profile name for the title.
|
|
@ObservedObject private var profiles = ProfileStore.shared
|
|
|
|
private var compact: Bool { vSizeClass == .compact }
|
|
|
|
var body: some View {
|
|
LibraryView(
|
|
store: store, target: target, onLaunch: onLaunch,
|
|
onClose: close, controllerActive: controllerActive)
|
|
.safeAreaInset(edge: .top, spacing: 0) {
|
|
// Leading, like every gamepad heading — no close chrome, B is the exit (the
|
|
// coverflow's, or LibraryView's own back-catcher before the coverflow exists).
|
|
Text("\(target.title(in: profiles)) — Library")
|
|
.font(.geist(gamepadTitleSize(compact: compact), .bold, relativeTo: .title))
|
|
.foregroundStyle(ink.fg)
|
|
.lineLimit(1)
|
|
.minimumScaleFactor(0.75)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(.horizontal, 24)
|
|
.padding(.top, gamepadTitleTopPadding(compact: compact))
|
|
.padding(.bottom, gamepadTitleBottomPadding(compact: compact))
|
|
.background { GamepadTrayBlur(edge: .top) }
|
|
}
|
|
// A hardware keyboard's Esc still closes, without chrome.
|
|
.background {
|
|
Button("Close") { close() }
|
|
.keyboardShortcut(.cancelAction)
|
|
.buttonStyle(.plain)
|
|
.frame(width: 0, height: 0)
|
|
.opacity(0)
|
|
.accessibilityHidden(true)
|
|
}
|
|
.gamepadPaletteInk()
|
|
}
|
|
}
|
|
#endif
|