Phase 9: Einrichtung, Autostart, Notarisierung — und die Mixer-Optik
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>
This commit is contained in:
@@ -12,6 +12,9 @@ import MetricsProvider
|
||||
import NetworkProvider
|
||||
import AudioProvider
|
||||
import ShelfProvider
|
||||
import OSLog
|
||||
|
||||
private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "App")
|
||||
|
||||
@main
|
||||
struct OnyxApp: App {
|
||||
@@ -44,6 +47,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private let settingsWindow = SettingsWindowController()
|
||||
private let fanControl = FanControl()
|
||||
private let audioMixer = AudioMixer()
|
||||
private let launchAtLogin = LaunchAtLogin()
|
||||
private let onboarding = OnboardingWindowController()
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
NSApp.setActivationPolicy(.accessory)
|
||||
@@ -113,12 +118,25 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
/// dort freischalten könnte. Genau dieser Zustand ist beim Testen
|
||||
/// aufgetreten.
|
||||
private func requestPermissions() {
|
||||
guard let calendarModel else { return }
|
||||
guard let calendarModel, let weatherModel else { return }
|
||||
log.notice("Start: Einrichtung erledigt = \(self.onboarding.isDone, privacy: .public)")
|
||||
// Immer einmal laden: mit Berechtigung kommen Termine, ohne sie zeigt
|
||||
// das Widget den Berechtigungshinweis statt einer leeren Liste.
|
||||
calendarModel.refresh()
|
||||
// Fenster öffnen, solange irgendeine Berechtigung noch aussteht.
|
||||
let needsLocation = weatherModel?.locationIsUndetermined ?? false
|
||||
|
||||
// Die Einrichtung kommt beim ersten Start immer — auch wenn zufällig
|
||||
// schon alles erlaubt ist. Sie erklärt nicht nur Berechtigungen,
|
||||
// sondern auch den Helfer und den Autostart, und sie ist der einzige
|
||||
// Moment, in dem jemand erfährt, was Onyx eigentlich alles kann.
|
||||
guard onboarding.isDone else {
|
||||
onboarding.show(calendarModel: calendarModel, weatherModel: weatherModel,
|
||||
mixer: audioMixer, fanControl: fanControl,
|
||||
launchAtLogin: launchAtLogin)
|
||||
return
|
||||
}
|
||||
|
||||
// Danach nur noch, wenn wirklich etwas fehlt.
|
||||
let needsLocation = weatherModel.locationIsUndetermined
|
||||
guard !calendarModel.hasAccess || needsLocation else { return }
|
||||
|
||||
// Nicht im Hintergrund anfragen.
|
||||
@@ -131,12 +149,20 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
// `NSApp.activate` davor hilft nicht: Aktivierung wirkt asynchron und
|
||||
// ist beim Aufruf noch nicht durch.
|
||||
//
|
||||
// Deshalb der ehrliche Weg: beim ersten Start das Einstellungsfenster
|
||||
// öffnen. Dort ist ein echtes Fenster im Vordergrund, der Dialog
|
||||
// erscheint zuverlässig — und der Nutzer weiß, warum gefragt wird.
|
||||
// Deshalb der ehrliche Weg: ein echtes Fenster im Vordergrund. Die
|
||||
// Einrichtung lief schon einmal, hier fehlt nur noch etwas — dann
|
||||
// genügt der Berechtigungsbereich der Einstellungen.
|
||||
showSettings(tab: .permissions)
|
||||
}
|
||||
|
||||
/// Die Einrichtung noch einmal zeigen — aus den Einstellungen heraus.
|
||||
private func showOnboarding() {
|
||||
guard let calendarModel, let weatherModel else { return }
|
||||
onboarding.show(calendarModel: calendarModel, weatherModel: weatherModel,
|
||||
mixer: audioMixer, fanControl: fanControl,
|
||||
launchAtLogin: launchAtLogin)
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
coordinator?.stop()
|
||||
menuBar?.stop()
|
||||
@@ -279,7 +305,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
guard let model, let calendarModel, let weatherModel else { return }
|
||||
settingsWindow.show(model: model, calendarModel: calendarModel,
|
||||
weatherModel: weatherModel, metricsModel: metricsModel,
|
||||
shelfStore: shelfStore, fanControl: fanControl, tab: tab)
|
||||
shelfStore: shelfStore, fanControl: fanControl,
|
||||
launchAtLogin: launchAtLogin,
|
||||
showOnboarding: { [weak self] in self?.showOnboarding() },
|
||||
tab: tab)
|
||||
}
|
||||
|
||||
@objc private func quit() { NSApp.terminate(nil) }
|
||||
|
||||
Reference in New Issue
Block a user