Phase 3: Kalender-Widget mit echten EventKit-Daten
Die Unterscheidung zwischen "keine Termine" und "keine Information" steckt von Anfang an in den Grundlagen, nicht als Nachtrag für Calendarr. 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. EventWindow trägt deshalb eine coverage mit. Bei EventKit ist sie der angefragte Zeitraum, weil direkt abgefragt wird; bei Calendarr wird sie später aus dem Snapshot kommen. Tage außerhalb werden im Mini-Monat ausgegraut und tragen einen erklärenden Hinweis, statt als frei durchzugehen. Die Abdeckung gewinnt auch gegen widersprüchliche Eingaben: liefert eine Quelle einen Termin für einen Tag, den sie nach eigener Angabe nicht abdeckt, bleibt der Tag unbekannt. Sonst behauptet die Anzeige mehr zu wissen, als belegt ist. Als Test festgehalten. CalendarSourceState statt eines Optionals: denied, neverWritten, loggedOut, incompatible und unreadable bekommen jeweils eine eigene Antwort in der Oberfläche. "Nichts anzuzeigen" ist keine. Serientermine bekommen eine aus Kennung und Beginn zusammengesetzte ID — EKEvent.eventIdentifier ist für alle Vorkommen gleich, eine Terminliste mit einer täglichen Serie fiele sonst auf einen Eintrag zusammen. Monatsraster testgetrieben, inklusive Schaltjahr, Wochenstart Montag wie Sonntag und der Regel, dass das Ende der Abdeckung nicht mehr dazugehört. 93 Tests grün.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
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)
|
||||
}
|
||||
|
||||
/// Eine Quelle für Termine. Apple-Kalender und Calendarr liefern beide dies.
|
||||
public protocol CalendarSource: Sendable {
|
||||
var id: String { get }
|
||||
func requestAccess() async -> Bool
|
||||
func calendars() async -> [OnyxCalendar]
|
||||
func load(_ interval: DateInterval) async -> CalendarSourceState
|
||||
}
|
||||
Reference in New Issue
Block a user