fix(apple/tvOS): television-idiomatic chrome — grid action tiles + full-screen covers
ci / rust (push) Has been cancelled
ci / rust (push) Has been cancelled
The iOS chrome half-worked on tvOS: toolbar items rendered tiny with clipped labels and could not even be focused (which is why "+" never opened the add-host form), and sheet presentations are not a tvOS idiom (the Settings form looked broken). - The toolbar is gone on tvOS. Add Host and Settings live IN the hosts grid as full-size, focus-native tiles (.card style, same geometry as the host cards) — the natural way actions work on television. - Every modal (Add Host, Settings, PIN pairing) presents as a fullScreenCover on tvOS; Settings gains a tvOS-only Done button (covers don't dismiss themselves). - iOS/macOS keep their existing toolbar + sheets untouched. Verified in the Apple TV simulator: title, host card and both action tiles render full-size and focusable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,17 @@ struct ContentView: View {
|
||||
// On the outer Group so the sheet survives the trust-prompt → home transition
|
||||
// (the "Pair with PIN instead" path disconnects first — the host's accept loop
|
||||
// is sequential, a pairing connection would queue behind the live session).
|
||||
#if os(tvOS)
|
||||
.fullScreenCover(item: $pairingTarget) { host in
|
||||
PairSheet(host: host) { fingerprint in
|
||||
guard pairingTarget?.id == host.id else { return }
|
||||
store.pin(host.id, fingerprint: fingerprint)
|
||||
var pinned = host
|
||||
pinned.pinnedSHA256 = fingerprint
|
||||
connect(pinned)
|
||||
}
|
||||
}
|
||||
#else
|
||||
.sheet(item: $pairingTarget) { host in
|
||||
PairSheet(host: host) { fingerprint in
|
||||
// Backstop against a stale ceremony surfacing after dismissal (PairSheet
|
||||
@@ -66,6 +77,7 @@ struct ContentView: View {
|
||||
connect(pinned)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private var sessionView: some View {
|
||||
@@ -114,14 +126,23 @@ struct ContentView: View {
|
||||
ForEach(store.hosts) { host in
|
||||
hostCard(host)
|
||||
}
|
||||
#if os(tvOS)
|
||||
actionTile("Add Host", systemImage: "plus") {
|
||||
showAddHost = true
|
||||
}
|
||||
actionTile("Settings", systemImage: "gearshape") {
|
||||
showSettings = true
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Punktfunkempfänger")
|
||||
#if !os(tvOS)
|
||||
.toolbar {
|
||||
#if !os(macOS)
|
||||
#if os(iOS)
|
||||
// Adjacent trailing items share one glass pill (the system default).
|
||||
ToolbarItem(placement: .topBarTrailing) { settingsButton }
|
||||
ToolbarItem(placement: .topBarTrailing) { addHostButton }
|
||||
@@ -138,14 +159,23 @@ struct ContentView: View {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#if os(macOS)
|
||||
.frame(minWidth: 480, minHeight: 360)
|
||||
#endif
|
||||
#if os(tvOS)
|
||||
.fullScreenCover(isPresented: $showAddHost) {
|
||||
AddHostSheet { store.add($0) }
|
||||
}
|
||||
.fullScreenCover(isPresented: $showSettings) {
|
||||
SettingsView()
|
||||
}
|
||||
#else
|
||||
.sheet(isPresented: $showAddHost) {
|
||||
AddHostSheet { store.add($0) }
|
||||
}
|
||||
#if !os(macOS)
|
||||
#if os(iOS)
|
||||
.sheet(isPresented: $showSettings) {
|
||||
NavigationStack {
|
||||
SettingsView()
|
||||
@@ -156,6 +186,7 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
.alert(
|
||||
"Connection failed",
|
||||
isPresented: Binding(
|
||||
@@ -306,6 +337,30 @@ struct ContentView: View {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(tvOS)
|
||||
/// Grid-resident replacement for the toolbar (whose items are neither sized nor
|
||||
/// focusable on tvOS): a full-size, focus-native tile per action.
|
||||
private func actionTile(
|
||||
_ label: String, systemImage: String, action: @escaping () -> Void
|
||||
) -> some View {
|
||||
Button(action: action) {
|
||||
VStack(spacing: 10) {
|
||||
Image(systemName: systemImage)
|
||||
.font(.system(size: 56, weight: .light))
|
||||
.foregroundStyle(.tint)
|
||||
.frame(height: 76)
|
||||
Text(label)
|
||||
.font(.title3.weight(.semibold))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 28)
|
||||
.padding(.horizontal, 12)
|
||||
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 14))
|
||||
}
|
||||
.buttonStyle(.card)
|
||||
}
|
||||
#endif
|
||||
|
||||
/// The host of the most recent session — its card carries the accent ring.
|
||||
private var mostRecentHostID: UUID? {
|
||||
store.hosts
|
||||
|
||||
@@ -9,6 +9,7 @@ import PunktfunkKit
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@AppStorage("punktfunk.width") private var width = 1920
|
||||
@AppStorage("punktfunk.height") private var height = 1080
|
||||
@AppStorage("punktfunk.hz") private var hz = 60
|
||||
@@ -99,6 +100,11 @@ struct SettingsView: View {
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
#if os(tvOS)
|
||||
Section {
|
||||
Button("Done") { dismiss() }
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
#if os(macOS)
|
||||
|
||||
Reference in New Issue
Block a user