Kalenderberechtigung: Anfrage aus dem Vordergrundfenster
TCC zeigt den Dialog nur, wenn eine App im Vordergrund ist, an die er gehören kann. Bei einer App ohne Fenster verpufft die Anfrage kommentarlos: sie kommt mit false zurück, der Status bleibt notDetermined, und in den Systemeinstellungen taucht die App nie auf — dort lässt sich auch nichts von Hand hinzufügen, Einträge entstehen ausschließlich durch eine echte Anfrage. Deshalb fragt der Reiter Berechtigungen jetzt selbst, sobald er sichtbar ist, mit 400 ms Vorlauf: Aktivierung wirkt asynchron, ohne die Pause fragt Onyx während es formal noch im Hintergrund liegt. CalendarAuthorization trennt undetermined von denied. Im ersten Fall lohnt eine Anfrage, im zweiten zeigt macOS nie wieder einen Dialog und es hilft nur der Weg über die Systemeinstellungen — dorthin führt der Knopf dann auch. Korrektur einer eigenen Fehldiagnose: promptPolicy = 0 im tccd-Protokoll gehört zu kTCCServiceReminders, nicht zum Kalender. Der bekommt durchgehend promptPolicy = 4, Nachfragen war also die ganze Zeit erlaubt. Die tatsächlichen Ursachen waren die ausgelagerte Debug-dylib und ein verklemmter TCC-Eintrag.
This commit is contained in:
@@ -150,6 +150,90 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings.permissions.calendar" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Kalender"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Calendar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.permissions.calendar.why" : {
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Onyx zeigt deine Termine im Kalender-Widget an. Es wird ausschließlich gelesen, nie geschrieben."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Onyx shows your events in the calendar widget. It only ever reads, never writes."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.permissions.granted" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Erteilt"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Granted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.permissions.openSystem" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Systemeinstellungen öffnen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Open System Settings"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings.permissions.request" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Anfragen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Request"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"settings.size.large" : {
|
"settings.size.large" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -235,6 +319,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"settings.tab.permissions" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
|
"localizations" : {
|
||||||
|
"de" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Berechtigungen"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Permissions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"settings.tab.widgets" : {
|
"settings.tab.widgets" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -351,102 +452,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"settings.tab.permissions": {
|
|
||||||
"localizations": {
|
|
||||||
"de": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Berechtigungen"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"en": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Permissions"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.permissions.calendar": {
|
|
||||||
"localizations": {
|
|
||||||
"de": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Kalender"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"en": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Calendar"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.permissions.granted": {
|
|
||||||
"localizations": {
|
|
||||||
"de": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Erteilt"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"en": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Granted"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.permissions.request": {
|
|
||||||
"localizations": {
|
|
||||||
"de": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Anfragen"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"en": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Request"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.permissions.calendar.why": {
|
|
||||||
"localizations": {
|
|
||||||
"de": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Onyx zeigt deine Termine im Kalender-Widget an. Es wird ausschließlich gelesen, nie geschrieben."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"en": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Onyx shows your events in the calendar widget. It only ever reads, never writes."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"settings.permissions.openSystem": {
|
|
||||||
"localizations": {
|
|
||||||
"de": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Systemeinstellungen öffnen"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"en": {
|
|
||||||
"stringUnit": {
|
|
||||||
"state": "translated",
|
|
||||||
"value": "Open System Settings"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.0"
|
"version" : "1.0"
|
||||||
|
|||||||
@@ -76,8 +76,23 @@ private struct PermissionSettings: View {
|
|||||||
}
|
}
|
||||||
.formStyle(.grouped)
|
.formStyle(.grouped)
|
||||||
.padding()
|
.padding()
|
||||||
.onAppear { granted = calendarModel.hasAccess }
|
.task {
|
||||||
|
granted = calendarModel.hasAccess
|
||||||
|
guard !granted, !didAutoRequest,
|
||||||
|
calendarModel.authorization == .undetermined else { return }
|
||||||
|
didAutoRequest = true
|
||||||
|
|
||||||
|
// Kurz warten, bis das Fenster wirklich vorne steht. TCC zeigt den
|
||||||
|
// Dialog nur, wenn eine App im Vordergrund ist, an die er gehören
|
||||||
|
// kann — und Aktivierung wirkt asynchron. Ohne diese Pause fragt
|
||||||
|
// Onyx, während es formal noch im Hintergrund liegt, und die
|
||||||
|
// Anfrage verpufft kommentarlos.
|
||||||
|
try? await Task.sleep(for: .milliseconds(400))
|
||||||
|
granted = await calendarModel.requestAccessNow()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@State private var didAutoRequest = false
|
||||||
|
|
||||||
private func openPrivacySettings() {
|
private func openPrivacySettings() {
|
||||||
guard let url = URL(string:
|
guard let url = URL(string:
|
||||||
|
|||||||
@@ -80,12 +80,25 @@ public enum CalendarSourceState: Equatable, Sendable {
|
|||||||
case unreadable(reason: String)
|
case unreadable(reason: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Der Berechtigungszustand einer Quelle.
|
||||||
|
///
|
||||||
|
/// `undetermined` und `denied` müssen getrennt bleiben: im ersten Fall lohnt
|
||||||
|
/// eine Anfrage, im zweiten zeigt macOS nie wieder einen Dialog und es hilft
|
||||||
|
/// nur der Weg über die Systemeinstellungen.
|
||||||
|
public enum CalendarAuthorization: Equatable, Sendable {
|
||||||
|
case granted
|
||||||
|
case denied
|
||||||
|
case undetermined
|
||||||
|
/// Die Quelle braucht gar keine Berechtigung (z. B. Calendarr).
|
||||||
|
case notRequired
|
||||||
|
}
|
||||||
|
|
||||||
/// Eine Quelle für Termine. Apple-Kalender und Calendarr liefern beide dies.
|
/// Eine Quelle für Termine. Apple-Kalender und Calendarr liefern beide dies.
|
||||||
public protocol CalendarSource: Sendable {
|
public protocol CalendarSource: Sendable {
|
||||||
var id: String { get }
|
var id: String { get }
|
||||||
/// Ob bereits gelesen werden darf — **ohne** dabei nachzufragen.
|
/// Der Zustand — **ohne** dabei nachzufragen. Die Einstellungen brauchen
|
||||||
/// Die Einstellungen brauchen das, um den Zustand anzuzeigen, ohne
|
/// das, um ihn anzuzeigen, ohne nebenbei einen Systemdialog auszulösen.
|
||||||
/// nebenbei einen Systemdialog auszulösen.
|
var authorization: CalendarAuthorization { get }
|
||||||
var isAuthorized: Bool { get }
|
var isAuthorized: Bool { get }
|
||||||
func requestAccess() async -> Bool
|
func requestAccess() async -> Bool
|
||||||
func calendars() async -> [OnyxCalendar]
|
func calendars() async -> [OnyxCalendar]
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public final class CalendarModel {
|
|||||||
|
|
||||||
/// Ob gelesen werden darf. Fragt nicht nach — nur zum Anzeigen.
|
/// Ob gelesen werden darf. Fragt nicht nach — nur zum Anzeigen.
|
||||||
public var hasAccess: Bool { source.isAuthorized }
|
public var hasAccess: Bool { source.isAuthorized }
|
||||||
|
public var authorization: CalendarAuthorization { source.authorization }
|
||||||
|
|
||||||
/// Fragt die Berechtigung an und lädt bei Erfolg gleich.
|
/// Fragt die Berechtigung an und lädt bei Erfolg gleich.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -16,9 +16,17 @@ public final class EventKitSource: CalendarSource, @unchecked Sendable {
|
|||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public var isAuthorized: Bool {
|
public var authorization: CalendarAuthorization {
|
||||||
EKEventStore.authorizationStatus(for: .event) == .fullAccess
|
switch EKEventStore.authorizationStatus(for: .event) {
|
||||||
|
case .fullAccess: .granted
|
||||||
|
case .denied, .restricted: .denied
|
||||||
|
// `writeOnly` ist für uns so gut wie nichts — Onyx zeigt nur an.
|
||||||
|
case .notDetermined, .writeOnly: .undetermined
|
||||||
|
@unknown default: .denied
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isAuthorized: Bool { authorization == .granted }
|
||||||
|
|
||||||
public func requestAccess() async -> Bool {
|
public func requestAccess() async -> Bool {
|
||||||
switch EKEventStore.authorizationStatus(for: .event) {
|
switch EKEventStore.authorizationStatus(for: .event) {
|
||||||
|
|||||||
Reference in New Issue
Block a user