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
|
// 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
|
// (the "Pair with PIN instead" path disconnects first — the host's accept loop
|
||||||
// is sequential, a pairing connection would queue behind the live session).
|
// 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
|
.sheet(item: $pairingTarget) { host in
|
||||||
PairSheet(host: host) { fingerprint in
|
PairSheet(host: host) { fingerprint in
|
||||||
// Backstop against a stale ceremony surfacing after dismissal (PairSheet
|
// Backstop against a stale ceremony surfacing after dismissal (PairSheet
|
||||||
@@ -66,6 +77,7 @@ struct ContentView: View {
|
|||||||
connect(pinned)
|
connect(pinned)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sessionView: some View {
|
private var sessionView: some View {
|
||||||
@@ -114,14 +126,23 @@ struct ContentView: View {
|
|||||||
ForEach(store.hosts) { host in
|
ForEach(store.hosts) { host in
|
||||||
hostCard(host)
|
hostCard(host)
|
||||||
}
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
actionTile("Add Host", systemImage: "plus") {
|
||||||
|
showAddHost = true
|
||||||
|
}
|
||||||
|
actionTile("Settings", systemImage: "gearshape") {
|
||||||
|
showSettings = true
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationTitle("Punktfunkempfänger")
|
.navigationTitle("Punktfunkempfänger")
|
||||||
|
#if !os(tvOS)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
#if !os(macOS)
|
#if os(iOS)
|
||||||
// Adjacent trailing items share one glass pill (the system default).
|
// Adjacent trailing items share one glass pill (the system default).
|
||||||
ToolbarItem(placement: .topBarTrailing) { settingsButton }
|
ToolbarItem(placement: .topBarTrailing) { settingsButton }
|
||||||
ToolbarItem(placement: .topBarTrailing) { addHostButton }
|
ToolbarItem(placement: .topBarTrailing) { addHostButton }
|
||||||
@@ -138,14 +159,23 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.frame(minWidth: 480, minHeight: 360)
|
.frame(minWidth: 480, minHeight: 360)
|
||||||
#endif
|
#endif
|
||||||
|
#if os(tvOS)
|
||||||
|
.fullScreenCover(isPresented: $showAddHost) {
|
||||||
|
AddHostSheet { store.add($0) }
|
||||||
|
}
|
||||||
|
.fullScreenCover(isPresented: $showSettings) {
|
||||||
|
SettingsView()
|
||||||
|
}
|
||||||
|
#else
|
||||||
.sheet(isPresented: $showAddHost) {
|
.sheet(isPresented: $showAddHost) {
|
||||||
AddHostSheet { store.add($0) }
|
AddHostSheet { store.add($0) }
|
||||||
}
|
}
|
||||||
#if !os(macOS)
|
#if os(iOS)
|
||||||
.sheet(isPresented: $showSettings) {
|
.sheet(isPresented: $showSettings) {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
@@ -156,6 +186,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
.alert(
|
.alert(
|
||||||
"Connection failed",
|
"Connection failed",
|
||||||
isPresented: Binding(
|
isPresented: Binding(
|
||||||
@@ -306,6 +337,30 @@ struct ContentView: View {
|
|||||||
#endif
|
#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.
|
/// The host of the most recent session — its card carries the accent ring.
|
||||||
private var mostRecentHostID: UUID? {
|
private var mostRecentHostID: UUID? {
|
||||||
store.hosts
|
store.hosts
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import PunktfunkKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct SettingsView: View {
|
struct SettingsView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
@AppStorage("punktfunk.width") private var width = 1920
|
@AppStorage("punktfunk.width") private var width = 1920
|
||||||
@AppStorage("punktfunk.height") private var height = 1080
|
@AppStorage("punktfunk.height") private var height = 1080
|
||||||
@AppStorage("punktfunk.hz") private var hz = 60
|
@AppStorage("punktfunk.hz") private var hz = 60
|
||||||
@@ -99,6 +100,11 @@ struct SettingsView: View {
|
|||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
Section {
|
||||||
|
Button("Done") { dismiss() }
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
|||||||
Reference in New Issue
Block a user