From 7dd479f9e4c0625612c276fc41dd2177b460e33c Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 11 Jun 2026 13:22:18 +0200 Subject: [PATCH] =?UTF-8?q?fix(apple/tvOS):=20television-idiomatic=20chrom?= =?UTF-8?q?e=20=E2=80=94=20grid=20action=20tiles=20+=20full-screen=20cover?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../Sources/PunktfunkClient/ContentView.swift | 59 ++++++++++++++++++- .../PunktfunkClient/SettingsView.swift | 6 ++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/clients/apple/Sources/PunktfunkClient/ContentView.swift b/clients/apple/Sources/PunktfunkClient/ContentView.swift index 3adabc6..cbc305f 100644 --- a/clients/apple/Sources/PunktfunkClient/ContentView.swift +++ b/clients/apple/Sources/PunktfunkClient/ContentView.swift @@ -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 diff --git a/clients/apple/Sources/PunktfunkClient/SettingsView.swift b/clients/apple/Sources/PunktfunkClient/SettingsView.swift index 06f3201..2bcff04 100644 --- a/clients/apple/Sources/PunktfunkClient/SettingsView.swift +++ b/clients/apple/Sources/PunktfunkClient/SettingsView.swift @@ -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)