Die Einrichtung beim ersten Start erklärt jede der vier Sachen einzeln und fragt jede einzeln an. Vier Systemdialoge hintereinander wären der übliche Weg und der falsche: wer nicht weiß wofür, klickt viermal „Nicht erlauben", und danach fragt macOS nie wieder. Sie kommt beim ersten Start immer, auch wenn zufällig schon alles erlaubt ist — sie erklärt auch den Helfer und den Autostart. Beim Autostart ist `requiresApproval` der Zustand, der weh tut. Wer das Anmeldeobjekt in den Systemeinstellungen abgeschaltet hat, kann in Onyx auf den Schalter drücken, so oft er will. Als „aus" angezeigt führt das in eine Schleife, also steht dort jetzt, wo es weitergeht. Der Mixer verliert den Schalter je Programm. Zehn Mini-Schalter untereinander sind das, was den Eindruck macht; am Regler zu ziehen ist ohnehin die Entscheidung, dieses Programm zu regeln, und der große Ausschalter stellt alles zurück. Der Regler ist selbst gezeichnet — der Systemregler bringt sein eigenes Erscheinungsbild mit und kann nicht zeigen, worauf es hier ankommt: den Pegel und die Grenze bei 100 %. Der Pegel wird im selben Durchlauf gemessen, in dem verstärkt wird, und über `Atomic` weitergereicht. Eine gewöhnliche Eigenschaft wäre ein Wettlauf, eine Sperre im Audiothread der klassische Weg zu Aussetzern. Dabei fällt das `Date()` aus dem Echtzeitpfad, das dort nie hingehört hat. Zwei Funde beim Bauen des Release-Wegs, beide hätten erst nach dem Hochladen zur Ablehnung geführt: der Helfer wurde ohne sicheren Zeitstempel signiert, und das Adapter-Framework ad-hoc. Beides gilt jetzt nur noch im Debug. Scripts/release.sh prüft Zertifikat und Notarisierungsprofil vorweg, statt nach zehn Minuten Bauen zu scheitern. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
78 lines
2.9 KiB
Swift
78 lines
2.9 KiB
Swift
import AppKit
|
|
import SwiftUI
|
|
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() }))
|
|
|
|
let window = NSWindow(contentViewController: hosting)
|
|
window.title = "Onyx"
|
|
window.styleMask = [.titled, .closable]
|
|
window.isReleasedWhenClosed = false
|
|
window.delegate = self
|
|
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)
|
|
window?.close()
|
|
}
|
|
|
|
func windowWillClose(_ notification: Notification) {
|
|
defaults.set(true, forKey: Self.doneKey)
|
|
// 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)
|
|
}
|
|
}
|
|
}
|