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.
107 lines
3.9 KiB
Swift
107 lines
3.9 KiB
Swift
import Foundation
|
|
|
|
/// Ein Termin, unabhängig davon, woher er stammt.
|
|
public struct OnyxEvent: Identifiable, Equatable, Sendable {
|
|
public let id: String
|
|
public let title: String
|
|
public let start: Date
|
|
public let end: Date
|
|
public let isAllDay: Bool
|
|
/// `#RRGGBB` der Quelle. Kalenderfarben sind die eine Stelle, an der Onyx
|
|
/// fremde Farben übernimmt statt eigene zu setzen — sonst erkennt man
|
|
/// seine Kalender nicht wieder.
|
|
public let colorHex: String?
|
|
public let location: String?
|
|
public let calendarID: String
|
|
public let calendarTitle: String
|
|
|
|
public init(id: String, title: String, start: Date, end: Date, isAllDay: Bool,
|
|
colorHex: String?, location: String?,
|
|
calendarID: String, calendarTitle: String) {
|
|
self.id = id
|
|
self.title = title
|
|
self.start = start
|
|
self.end = end
|
|
self.isAllDay = isAllDay
|
|
self.colorHex = colorHex
|
|
self.location = location
|
|
self.calendarID = calendarID
|
|
self.calendarTitle = calendarTitle
|
|
}
|
|
}
|
|
|
|
public struct OnyxCalendar: Identifiable, Equatable, Sendable {
|
|
public let id: String
|
|
public let title: String
|
|
public let colorHex: String?
|
|
|
|
public init(id: String, title: String, colorHex: String?) {
|
|
self.id = id
|
|
self.title = title
|
|
self.colorHex = colorHex
|
|
}
|
|
}
|
|
|
|
/// Termine **samt der Angabe, wofür überhaupt Daten vorliegen.**
|
|
///
|
|
/// Das ist der Kern und nicht ein Detail: ein Kalender ohne Termine und ein
|
|
/// Kalender, über den nichts bekannt ist, sehen in einer Liste identisch aus —
|
|
/// nämlich leer. Ein leerer März ist überzeugend und schlicht falsch, wenn die
|
|
/// Quelle nur 49 Tage abdeckt. `coverage` trennt beides.
|
|
public struct EventWindow: Equatable, Sendable {
|
|
public let events: [OnyxEvent]
|
|
/// Zeitraum mit gesicherter Information. `nil` bedeutet unbegrenzt —
|
|
/// so verhält sich der Apple-Kalender, weil er direkt abgefragt wird.
|
|
public let coverage: DateInterval?
|
|
/// Stand der Daten, falls die Quelle zwischengespeichert liefert.
|
|
public let asOf: Date?
|
|
|
|
public init(events: [OnyxEvent], coverage: DateInterval? = nil, asOf: Date? = nil) {
|
|
self.events = events
|
|
self.coverage = coverage
|
|
self.asOf = asOf
|
|
}
|
|
|
|
public static let empty = EventWindow(events: [])
|
|
}
|
|
|
|
/// Warum gerade keine Termine da sind. Jeder Fall braucht eine eigene Antwort
|
|
/// in der Oberfläche — „nichts anzuzeigen" ist keine.
|
|
public enum CalendarSourceState: Equatable, Sendable {
|
|
case ready(EventWindow)
|
|
/// Berechtigung nicht erteilt.
|
|
case denied
|
|
/// Die Quelle wurde eingerichtet, hat aber noch nie geschrieben.
|
|
case neverWritten
|
|
/// Angemeldet sein wäre nötig.
|
|
case loggedOut(asOf: Date?)
|
|
/// Die Quelle ist neuer als das, was Onyx lesen kann.
|
|
case incompatible(found: Int, maxSupported: Int)
|
|
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.
|
|
public protocol CalendarSource: Sendable {
|
|
var id: String { get }
|
|
/// Der Zustand — **ohne** dabei nachzufragen. Die Einstellungen brauchen
|
|
/// das, um ihn anzuzeigen, ohne nebenbei einen Systemdialog auszulösen.
|
|
var authorization: CalendarAuthorization { get }
|
|
var isAuthorized: Bool { get }
|
|
func requestAccess() async -> Bool
|
|
func calendars() async -> [OnyxCalendar]
|
|
func load(_ interval: DateInterval) async -> CalendarSourceState
|
|
}
|