1eef55016d
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>
84 lines
4.1 KiB
Swift
84 lines
4.1 KiB
Swift
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
|