import AppKit import SwiftUI import OnyxDesign import CalendarProvider import WeatherProvider import AudioProvider /// Zeigt das Onboarding — einmal, beim ersten Start. /// /// Als eigenes Fenster und nicht im Panel: TCC blendet Berechtigungsdialoge /// stillschweigend aus, wenn keine App im Vordergrund steht, und eine /// `.accessory`-App mit ausgefahrenem Panel steht nicht im Vordergrund. Genau /// daran ist die Kalenderberechtigung schon einmal gescheitert. @MainActor final class OnboardingWindowController: NSObject, NSWindowDelegate { private static let doneKey = "onboarding.completed" private var window: NSWindow? private let defaults: UserDefaults init(defaults: UserDefaults = .standard) { self.defaults = defaults } var isDone: Bool { defaults.bool(forKey: Self.doneKey) } func showIfNeeded(calendarModel: CalendarModel, weatherModel: WeatherModel, mixer: AudioMixer, fanControl: FanControl, launchAtLogin: LaunchAtLogin) { guard !isDone else { return } show(calendarModel: calendarModel, weatherModel: weatherModel, mixer: mixer, fanControl: fanControl, launchAtLogin: launchAtLogin) } func show(calendarModel: CalendarModel, weatherModel: WeatherModel, mixer: AudioMixer, fanControl: FanControl, launchAtLogin: LaunchAtLogin) { // Solange dieses Fenster steht, gehört Onyx in den Dock und in den // Vordergrund — sonst erscheinen die Systemdialoge gar nicht. NSApp.setActivationPolicy(.regular) if window == nil { let hosting = NSHostingController( rootView: OnboardingView( calendarModel: calendarModel, weatherModel: weatherModel, mixer: mixer, fanControl: fanControl, launchAtLogin: launchAtLogin, onFinish: { [weak self] in self?.finish() }) .tint(Onyx.Color.accent) .labelStyle(.onyx)) let window = NSWindow(contentViewController: hosting) window.title = "Onyx" window.styleMask = [.titled, .closable] window.isReleasedWhenClosed = false window.delegate = self // Nicht direkt unter die Notch: wer den Schließknopf trifft, // fährt sonst durch die Auslösefläche. if let screen = NSScreen.main { let visible = screen.visibleFrame window.setFrameOrigin(NSPoint(x: visible.midX - window.frame.width / 2, y: visible.maxY - 60 - window.frame.height)) } else { window.center() } self.window = window } NSApp.activate(ignoringOtherApps: true) window?.makeKeyAndOrderFront(nil) } /// Auch das Schließen über den roten Knopf gilt als erledigt. Ein Fenster, /// das beim nächsten Start wiederkommt, weil man es weggeklickt hat, ist /// eine Zumutung. private func finish() { defaults.set(true, forKey: Self.doneKey) hide() } /// Wie bei den Einstellungen: ausblenden statt schließen, damit ein /// Hilfsprogramm mit „letztes Fenster zu = App beenden" Onyx nicht mit /// wegräumt. func windowShouldClose(_ sender: NSWindow) -> Bool { defaults.set(true, forKey: Self.doneKey) hide() return false } private func hide() { window?.orderOut(nil) // Zurück in den Hintergrund — es sei denn, ein anderes Fenster steht // noch offen. if !NSApp.windows.contains(where: { $0 !== window && $0.isVisible }) { NSApp.setActivationPolicy(.accessory) } } }