feat(apple): tabbed macOS Settings + stats-overlay placement/toggle + Stream menu

The macOS Settings window had outgrown one scrolling pane — split it into a tabbed
preferences window (General / Display / Audio / Controllers / Advanced). Each
settings group is now a shared @ViewBuilder section, so iOS keeps its single
grouped Form and tvOS its pushed-picker layout, each defined once. No setting
moved or dropped.

New statistics-overlay controls (Settings → Display → Statistics): a show/hide
toggle (DefaultsKey.hudEnabled) and a corner picker (HUDPlacement /
DefaultsKey.hudPlacement) — the HUD moves to the chosen corner and aligns its text
to that edge.

A Scene-level "Stream" menu (StreamCommands) carries Show/Hide Statistics (⌘⇧S)
and Disconnect (⌘D). Disconnect moved off the HUD button into the menu so it
survives the overlay being hidden, wired via .focusedSceneValue. On iOS a
material-backed exit chip appears when the HUD is hidden (touch users have no
menu/⌘D); tvOS disconnect is unchanged (Siri-Remote Menu button).

Builds on macOS/iOS/tvOS; swift test green. Adversarially reviewed (8 findings
refuted, 2 minor — the iOS exit-chip contrast fix is included here).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 22:11:39 +02:00
parent 5e665c2c3e
commit 2b01294eb7
7 changed files with 448 additions and 196 deletions
@@ -4,12 +4,44 @@
import PunktfunkKit
import SwiftUI
/// Which corner the HUD overlay occupies (persisted as `DefaultsKey.hudPlacement`). The raw
/// values are stable on disk rename the cases freely, never the strings.
enum HUDPlacement: String, CaseIterable, Identifiable {
case topLeading, topTrailing, bottomLeading, bottomTrailing
var id: String { rawValue }
/// SwiftUI overlay alignment for `.overlay(alignment:)`.
var alignment: Alignment {
switch self {
case .topLeading: return .topLeading
case .topTrailing: return .topTrailing
case .bottomLeading: return .bottomLeading
case .bottomTrailing: return .bottomTrailing
}
}
/// The HUD's own stack hugs the screen edge it sits against, so its text aligns outward.
var isTrailing: Bool { self == .topTrailing || self == .bottomTrailing }
/// User-facing corner label.
var label: String {
switch self {
case .topLeading: return "Top Left"
case .topTrailing: return "Top Right"
case .bottomLeading: return "Bottom Left"
case .bottomTrailing: return "Bottom Right"
}
}
}
struct StreamHUDView: View {
@ObservedObject var model: SessionModel
let connection: PunktfunkConnection
var placement: HUDPlacement = .topTrailing
var body: some View {
VStack(alignment: .trailing, spacing: 4) {
VStack(alignment: placement.isTrailing ? .trailing : .leading, spacing: 4) {
HStack(spacing: 6) {
Circle()
.fill(Color.accentColor)
@@ -60,9 +92,10 @@ struct StreamHUDView: View {
.font(.caption)
.foregroundStyle(.secondary)
#else
// D lives on the app's Stream menu (so it still works when the HUD is hidden);
// this button is the in-overlay, click-to-disconnect affordance.
Button("Disconnect (⌘D)") { model.disconnect() }
.font(.caption)
.keyboardShortcut("d", modifiers: .command)
#endif
}
.padding(10)