Berechtigungsdialog und Einstellungsfenster reparieren
Zwei Fehler, beide durch Messen gefunden statt durch Raten — und einer davon war meine eigene fehlerhafte Diagnose: `log show` wurde von zsh abgefangen, ich hatte stderr umgeleitet und die Fehlermeldung nie gesehen. Meine Aussage "keine TCC-Einträge, also wurde nie gefragt" stützte sich damit auf einen Befehl, der gar nicht lief. Einstellungsfenster: SwiftUIs Settings-Szene wird über den privaten Selektor showSettingsWindow: geöffnet, dessen Name sich zwischen macOS-Versionen schon geändert hat und der bei .accessory-Apps ohne Menüleiste unzuverlässig ankommt. Der Menüeintrag tat schlicht nichts. Jetzt ein eigenes NSWindow. Kalenderberechtigung, zwei Ursachen übereinander: Xcode lagert im Debug-Build den Programmcode in eine eigene dylib aus (für SwiftUI-Vorschauen). Das Hauptprogramm ist dann nur ein Rumpf, und TCC ordnete die Anfrage dem Rumpf zu — kTCCServiceCalendar erreichte tccd überhaupt nicht. ENABLE_DEBUG_DYLIB = NO behebt das; danach erscheint die Anfrage im Protokoll. Danach immer noch kein Dialog: promptType 1, aber promptPolicy 0. TCC bereitet den Dialog vor und entscheidet sich dagegen, weil keine App im Vordergrund ist, an die er gehören könnte. NSApp.activate davor hilft nicht — Aktivierung wirkt asynchron und ist beim Aufruf noch nicht durch. Deshalb wird beim ersten Start das Einstellungsfenster auf dem Reiter Berechtigungen geöffnet: dort steht ein echtes Fenster im Vordergrund, und der Nutzer weiß, warum gefragt wird. Folgefehler dabei: ohne Berechtigung zeigte das Widget "Nichts mehr für heute" — eine Aussage, die es ohne Zugriff gar nicht treffen kann. refresh() fragt jetzt nicht mehr selbst nach und meldet ohne Berechtigung ehrlich `denied`. Fehler aus requestFullAccessToEvents werden nicht mehr mit try? verschluckt.
This commit is contained in:
@@ -83,6 +83,10 @@ public enum CalendarSourceState: Equatable, Sendable {
|
||||
/// Eine Quelle für Termine. Apple-Kalender und Calendarr liefern beide dies.
|
||||
public protocol CalendarSource: Sendable {
|
||||
var id: String { get }
|
||||
/// Ob bereits gelesen werden darf — **ohne** dabei nachzufragen.
|
||||
/// Die Einstellungen brauchen das, um den Zustand anzuzeigen, ohne
|
||||
/// nebenbei einen Systemdialog auszulösen.
|
||||
var isAuthorized: Bool { get }
|
||||
func requestAccess() async -> Bool
|
||||
func calendars() async -> [OnyxCalendar]
|
||||
func load(_ interval: DateInterval) async -> CalendarSourceState
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import SwiftUI
|
||||
import OSLog
|
||||
import EventKit
|
||||
import OnyxDesign
|
||||
import OnyxWidgetKit
|
||||
|
||||
private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "Calendar")
|
||||
|
||||
/// Hält die Termine für das Widget und lädt sie nach.
|
||||
@MainActor
|
||||
@Observable
|
||||
@@ -19,6 +23,24 @@ public final class CalendarModel {
|
||||
self.calendar = calendar
|
||||
}
|
||||
|
||||
/// Ob gelesen werden darf. Fragt nicht nach — nur zum Anzeigen.
|
||||
public var hasAccess: Bool { source.isAuthorized }
|
||||
|
||||
/// Fragt die Berechtigung an und lädt bei Erfolg gleich.
|
||||
///
|
||||
/// Wird beim Start gerufen, nicht erst wenn das Widget sichtbar wird. Das
|
||||
/// Panel ist beim Start zu, also erscheint eine View-gebundene Anfrage erst,
|
||||
/// wenn jemand die Notch berührt — und bis dahin steht in den
|
||||
/// Systemeinstellungen kein Onyx, das man freischalten könnte.
|
||||
@discardableResult
|
||||
public func requestAccessNow() async -> Bool {
|
||||
log.notice("Kalenderzugriff: Status vor der Anfrage = \(EKEventStore.authorizationStatus(for: .event).rawValue, privacy: .public)")
|
||||
let granted = await source.requestAccess()
|
||||
log.notice("Kalenderzugriff: erteilt = \(granted, privacy: .public), Status danach = \(EKEventStore.authorizationStatus(for: .event).rawValue, privacy: .public)")
|
||||
if granted { refresh() } else { state = .denied }
|
||||
return granted
|
||||
}
|
||||
|
||||
public var window: EventWindow? {
|
||||
if case .ready(let window) = state { return window }
|
||||
return nil
|
||||
@@ -33,7 +55,12 @@ public final class CalendarModel {
|
||||
isLoading = true
|
||||
defer { isLoading = false }
|
||||
|
||||
guard await source.requestAccess() else {
|
||||
// Hier wird NICHT nachgefragt. Ein Berechtigungsdialog aus einer
|
||||
// App ohne Fenster heraus wird von TCC verworfen, ohne dass etwas
|
||||
// passiert — die Anfrage muss aus dem Einstellungsfenster kommen.
|
||||
// Ohne Berechtigung ist der ehrliche Zustand `denied` und nicht
|
||||
// eine leere Terminliste, die "nichts mehr heute" behaupten würde.
|
||||
guard source.isAuthorized else {
|
||||
state = .denied
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import Foundation
|
||||
import OSLog
|
||||
import EventKit
|
||||
import AppKit
|
||||
|
||||
private let log = Logger(subsystem: "com.scarriffleservices.onyx", category: "EventKit")
|
||||
|
||||
/// Termine aus dem Apple-Kalender. Ausschließlich lesend.
|
||||
public final class EventKitSource: CalendarSource, @unchecked Sendable {
|
||||
|
||||
@@ -13,6 +16,10 @@ public final class EventKitSource: CalendarSource, @unchecked Sendable {
|
||||
|
||||
public init() {}
|
||||
|
||||
public var isAuthorized: Bool {
|
||||
EKEventStore.authorizationStatus(for: .event) == .fullAccess
|
||||
}
|
||||
|
||||
public func requestAccess() async -> Bool {
|
||||
switch EKEventStore.authorizationStatus(for: .event) {
|
||||
case .fullAccess:
|
||||
@@ -22,7 +29,15 @@ public final class EventKitSource: CalendarSource, @unchecked Sendable {
|
||||
case .notDetermined, .writeOnly:
|
||||
// `writeOnly` ist für uns wertlos — Onyx zeigt nur an. Der Nutzer
|
||||
// muss den Vollzugriff geben, sonst bleibt das Widget leer.
|
||||
return (try? await store.requestFullAccessToEvents()) ?? false
|
||||
do {
|
||||
return try await store.requestFullAccessToEvents()
|
||||
} catch {
|
||||
// Nicht verschlucken: wenn EventKit hier wirft, erscheint gar
|
||||
// kein Dialog und der Zustand bleibt `notDetermined` — von außen
|
||||
// sieht das aus, als hätte die App nie gefragt.
|
||||
log.error("requestFullAccessToEvents warf: \(error.localizedDescription, privacy: .public) — \(String(describing: error), privacy: .public)")
|
||||
return false
|
||||
}
|
||||
@unknown default:
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,62 +1,159 @@
|
||||
{
|
||||
"sourceLanguage" : "en",
|
||||
"strings" : {
|
||||
"widget.calendar.name" : {
|
||||
"localizations" : {
|
||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Kalender" } },
|
||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Calendar" } }
|
||||
}
|
||||
"" : {
|
||||
|
||||
},
|
||||
"widget.calendar.noUpcoming" : {
|
||||
"comment" : "Nur zeigen, wenn der Zeitraum wirklich abgedeckt ist",
|
||||
"localizations" : {
|
||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Nichts mehr für heute" } },
|
||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Nothing left today" } }
|
||||
}
|
||||
"%lld" : {
|
||||
"comment" : "A day of the month. The argument is the day of the month.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"widget.calendar.allDay" : {
|
||||
"localizations" : {
|
||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Ganztägig" } },
|
||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "All day" } }
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Ganztägig"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "All day"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"widget.calendar.day.unknown" : {
|
||||
"comment" : "Erklärt einen ausgegrauten Tag: keine Information, nicht keine Termine",
|
||||
"localizations" : {
|
||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Für diesen Tag liegen keine Daten vor" } },
|
||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "No data available for this day" } }
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Für diesen Tag liegen keine Daten vor"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "No data available for this day"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"widget.calendar.denied" : {
|
||||
"localizations" : {
|
||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Kein Kalenderzugriff.\nIn den Systemeinstellungen erlauben." } },
|
||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "No calendar access.\nAllow it in System Settings." } }
|
||||
}
|
||||
},
|
||||
"widget.calendar.neverWritten" : {
|
||||
"localizations" : {
|
||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Calendarr einmal öffnen,\ndamit Termine bereitstehen." } },
|
||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Open Calendarr once\nso events become available." } }
|
||||
}
|
||||
},
|
||||
"widget.calendar.loggedOut" : {
|
||||
"localizations" : {
|
||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "In Calendarr anmelden" } },
|
||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Sign in to Calendarr" } }
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Kein Kalenderzugriff.\nIn den Systemeinstellungen erlauben."
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "No calendar access.\nAllow it in System Settings."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"widget.calendar.incompatible" : {
|
||||
"localizations" : {
|
||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Onyx ist zu alt für diese\nCalendarr-Version. Bitte aktualisieren." } },
|
||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Onyx is too old for this\nCalendarr version. Please update." } }
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Onyx ist zu alt für diese\nCalendarr-Version. Bitte aktualisieren."
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Onyx is too old for this\nCalendarr version. Please update."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"widget.calendar.loggedOut" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "In Calendarr anmelden"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sign in to Calendarr"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"widget.calendar.name" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Kalender"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Calendar"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"widget.calendar.neverWritten" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Calendarr einmal öffnen,\ndamit Termine bereitstehen."
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Open Calendarr once\nso events become available."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"widget.calendar.noUpcoming" : {
|
||||
"comment" : "Nur zeigen, wenn der Zeitraum wirklich abgedeckt ist",
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nichts mehr für heute"
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Nothing left today"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"widget.calendar.unreadable" : {
|
||||
"localizations" : {
|
||||
"de" : { "stringUnit" : { "state" : "translated", "value" : "Termine nicht lesbar.\nKonfigurationsfehler." } },
|
||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "Events unreadable.\nConfiguration error." } }
|
||||
"de" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Termine nicht lesbar.\nKonfigurationsfehler."
|
||||
}
|
||||
},
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Events unreadable.\nConfiguration error."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
"version" : "1.1"
|
||||
}
|
||||
Reference in New Issue
Block a user