refactor(apple/W8): extract FullscreenController + ApprovalRequest from ContentView

Move the macOS `FullscreenController` (NSViewRepresentable that drives native
fullscreen) into its own FullscreenController.swift, and `ApprovalRequest` (the
pending-trust-decision value type) into ApprovalRequest.swift, out of the
1041-line ContentView.swift. Both were file-`private`; dropped to internal so
ContentView (same module) still references them across files. StreamView*.swift
and the connection body are untouched. Pure move; no behavior change.

Verified: `swift build` (macOS) — Build complete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 15:02:34 +02:00
parent 61118cbdd4
commit 1eef55016d
3 changed files with 115 additions and 76 deletions
@@ -0,0 +1,83 @@
import PunktfunkKit
import SwiftUI
#if os(macOS)
import AppKit
/// Drives the hosting window in/out of native fullscreen from SwiftUI state, and mirrors the
/// window's ACTUAL fullscreen state back into `isFullscreen` (the user can also toggle it with the
/// green button / F ContentView keys the session view's safe-area handling off the real state,
/// not the setting). Mounted invisibly in the view tree; on each `active` change it captures the
/// window and toggles fullscreen only when the current state differs (so it never fights a toggle
/// already in flight, and never touches a window the user fullscreened manually unless `active`
/// says otherwise).
struct FullscreenController: NSViewRepresentable {
let active: Bool
@Binding var isFullscreen: Bool
/// Holds the window's fullscreen-transition observers so they're rebound on a window change
/// and removed on dismantle.
final class Coordinator {
var observers: [NSObjectProtocol] = []
weak var observedWindow: NSWindow?
/// The last `active` value we DROVE the window to. We toggle only when `active` itself
/// changes (stream start/end) never to correct a mismatch so a deliberate mid-session
/// toggle (F / the green button) isn't snapped back on the next SwiftUI update.
var lastActive: Bool?
deinit { observers.forEach(NotificationCenter.default.removeObserver(_:)) }
}
func makeCoordinator() -> Coordinator { Coordinator() }
func makeNSView(context: Context) -> NSView { NSView() }
func updateNSView(_ view: NSView, context: Context) {
let want = active
let isFullscreen = $isFullscreen
let coordinator = context.coordinator
DispatchQueue.main.async {
guard let window = view.window else { return }
observeTransitions(of: window, coordinator: coordinator)
let isFull = window.styleMask.contains(.fullScreen)
if isFullscreen.wrappedValue != isFull { isFullscreen.wrappedValue = isFull }
// Drive the window only on an `active` EDGE (stream start/end), not to close a mismatch
// so a user's F / green-button toggle stays put. First pass (lastActive == nil) just
// records the state without toggling, so mounting never yanks a window into fullscreen.
if coordinator.lastActive != want {
coordinator.lastActive = want
if want != isFull { window.toggleFullScreen(nil) }
}
}
}
/// `willEnter` (not did) so the video goes edge-to-edge while the title bar is already
/// animating away; `didExit` so the top inset returns only once the title bar is back
/// no black gap in either direction.
private func observeTransitions(of window: NSWindow, coordinator: Coordinator) {
guard coordinator.observedWindow !== window else { return }
coordinator.observers.forEach(NotificationCenter.default.removeObserver(_:))
coordinator.observers.removeAll()
coordinator.observedWindow = window
let isFullscreen = $isFullscreen
for (name, value) in [
(NSWindow.willEnterFullScreenNotification, true),
(NSWindow.didExitFullScreenNotification, false),
] {
coordinator.observers.append(NotificationCenter.default.addObserver(
forName: name, object: window, queue: .main
) { _ in
isFullscreen.wrappedValue = value
})
}
// The Stream menu's "Toggle Fullscreen" (F) and InputCapture's captured-state interception
// both post this; flip the KEY window only (posted app-wide, object nil). The transition
// observers above then mirror the real state back into the binding.
coordinator.observers.append(NotificationCenter.default.addObserver(
forName: .punktfunkToggleFullscreen, object: nil, queue: .main
) { [weak window] _ in
guard let window, window.isKeyWindow else { return }
window.toggleFullScreen(nil)
})
}
}
#endif