feat(apple): gamepad UI v2 — controller settings + add host, aurora, macOS
Sources reorganized (client: Home/Session/Settings/Stores/Support/Trust; kit: Audio/Connection/Gamepad/Input/Support/Video/Views) with the big files split along the same seams. The gamepad mode is couch-complete, and now on macOS too (the living-room Mac case), not just iOS/iPadOS: - GamepadSettingsView: a console-style, fully controller-navigable settings screen (X from the launcher) — up/down moves focus, left/right steps values (clamped, boundary thud), A cycles/toggles, B closes; the focused row shows a one-line description. Backed by GamepadMenuList, the vertical sibling of GamepadCarousel, and SettingsOptions — the option lists hoisted out of SettingsView statics and shared by the touch, tvOS and gamepad settings. - GamepadAddHostView + GamepadKeyboard: register a host end to end with a pad — field rows open an on-screen controller keyboard (dpad grid, A types, X backspaces, B done); the launcher carousel ends in an Add Host tile, so the dead-end "add one with touch first" empty state is gone. - Launcher polish: contextual hint bar with the pad's real button glyphs, controller name + battery chip, one shared console chrome. - GamepadScreenBackground: an animated aurora (TimelineView-driven drifting blobs in the brand's violet family, breathing radii, slow hue shift, legibility scrim; freezes under Reduce Motion). Pure SwiftUI on purpose — a .metal library only bundles reliably in one of the two build systems (SPM vs the xcodeproj's synced folders) these sources compile under. - macOS port: settings/add-host/library present as sized sheets (a macOS sheet takes its content's IDEAL size, and the GeometryReader-driven screens collapsed to nothing), NSScreen-based mode lists, scroll indicators .never (the "always show scroll bars" setting overrides .hidden), tray scrims so scrolled rows dim under the pinned title/hints, extra title clearance, and a PUNKTFUNK_FORCE_GAMEPAD_UI=1 dev hook — launcher/settings/add-host/keyboard/ library render-verified live on a real Mac + LAN hosts. - GamepadMenuInput: X button support, and (re)start now snapshots held buttons so a controller handoff press never fires twice (the B that closed the keyboard no longer also cancels the screen underneath). - Cleanups: one "Connection failed" alert in ContentView instead of one per home screen; HostDiscovery.advertises/unsaved shared by both home screens. - host: can_encode_444 stub for the non-Linux/Windows host build (the macOS synthetic-source loopback used by the Swift tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
// Trust-on-first-use prompt: shown over the live-but-blurred stream when connecting to an
|
||||
// unpinned host. The user compares the fingerprint with the one the host logged at startup,
|
||||
// or drops this and runs the PIN pairing ceremony instead.
|
||||
|
||||
import Foundation
|
||||
import PunktfunkKit
|
||||
import SwiftUI
|
||||
|
||||
struct TrustCardView: View {
|
||||
let fingerprint: Data
|
||||
let hostName: String
|
||||
let onCancel: () -> Void
|
||||
let onTrust: () -> Void
|
||||
let onPairInstead: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 14) {
|
||||
Image(systemName: "lock.shield")
|
||||
.font(.system(size: 36, weight: .light))
|
||||
.foregroundStyle(.tint)
|
||||
Text("Verify \(hostName)")
|
||||
.font(.geist(20, .semibold, relativeTo: .title3))
|
||||
Text("First connection. Compare this fingerprint with the one "
|
||||
+ "punktfunk-host logged at startup (\u{201C}clients pin this "
|
||||
+ "fingerprint\u{201D}):")
|
||||
.font(.geist(16, relativeTo: .callout))
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
Text(Self.format(fingerprint: fingerprint))
|
||||
.font(.system(.callout, design: .monospaced))
|
||||
#if !os(tvOS)
|
||||
.textSelection(.enabled)
|
||||
#endif
|
||||
.padding(10)
|
||||
.background(.quaternary, in: RoundedRectangle(cornerRadius: 8))
|
||||
HStack(spacing: 12) {
|
||||
Button("Cancel", role: .cancel, action: onCancel)
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.cancelAction)
|
||||
#endif
|
||||
Button("Trust & Connect", action: onTrust)
|
||||
// Opaque prominent, NOT glass: this card is itself a glass panel
|
||||
// (.glassBackground below), and glass-on-glass loses contrast — a tinted
|
||||
// bordered button reads cleanly over glass (HIG). The sheet primaries stay
|
||||
// glass because the system manages the sheet's own glass layering.
|
||||
.buttonStyle(.borderedProminent)
|
||||
#if !os(tvOS)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
#endif
|
||||
}
|
||||
#if os(iOS)
|
||||
.controlSize(.large)
|
||||
#endif
|
||||
// The verified alternative to eyeballing hex: drop this session (the host
|
||||
// serves one connection at a time) and run the SPAKE2 PIN ceremony instead.
|
||||
Button("Pair with PIN instead…", action: onPairInstead)
|
||||
#if os(macOS)
|
||||
.buttonStyle(.link)
|
||||
#else
|
||||
.buttonStyle(.borderless)
|
||||
#endif
|
||||
.font(.geist(16, relativeTo: .callout))
|
||||
}
|
||||
.padding(28)
|
||||
.frame(maxWidth: 440)
|
||||
// Floating trust card over the blurred stream — Liquid Glass on 26+, .regularMaterial
|
||||
// fallback below. The inner fingerprint box stays .quaternary (content, not glass).
|
||||
.glassBackground(RoundedRectangle(cornerRadius: 18))
|
||||
}
|
||||
|
||||
/// 64 hex chars → four groups per line, two lines — easy to eyeball against the log.
|
||||
private static func format(fingerprint: Data) -> String {
|
||||
let hex = fingerprint.hexLower
|
||||
let groups = stride(from: 0, to: hex.count, by: 8).map { i -> String in
|
||||
let start = hex.index(hex.startIndex, offsetBy: i)
|
||||
let end = hex.index(start, offsetBy: min(8, hex.count - i))
|
||||
return String(hex[start..<end])
|
||||
}
|
||||
return groups.chunks(of: 4).map { $0.joined(separator: " ") }.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array {
|
||||
func chunks(of size: Int) -> [[Element]] {
|
||||
stride(from: 0, to: count, by: size).map { Array(self[$0..<Swift.min($0 + size, count)]) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user