Files
onyx/Packages/OnyxKit/Sources/CalendarProvider/CalendarSourceKind.swift
Scarriffle 49e1a46928 Widget-Optionen statt „Quellen", Reiterleiste von der Trennlinie herunter
Die Leiste saß mittig auf der Trennlinie unter dem Titelbalken.
`titlebarAppearsTransparent` allein reichte nicht — macOS zieht darunter
zusätzlich einen Trenner quer durchs Fenster. Der ist jetzt aus. Man setzt
keinen Titel auf eine Trennlinie.

„Quellen" war die Sicht des Programmierers: für den Nutzer ist die
Datenquelle eine Einstellung **des Kalenderwidgets** unter mehreren. Der
Bereich heißt jetzt „Widget-Optionen" und hat einen Abschnitt je Widget.

Kalender: woher die Daten kommen, Monatsraster oder Terminliste — und
endlich, welche Kalender überhaupt mitzählen. Gespeichert werden die
**abgewählten**, nicht die gewählten: kommt ein neuer Kalender dazu, ist er
damit automatisch dabei, statt dass man ihn suchen müsste, ohne zu wissen,
dass es ihn gibt. Der Filter lässt die Abdeckung unberührt — welche Kalender
man sehen will, sagt nichts darüber aus, für welchen Zeitraum Daten
vorliegen.

Wetter: dem Standort folgen oder einen festen Ort setzen, per Stadt oder
Postleitzahl. WeatherKit will Koordinaten, der Nutzer kennt Ortsnamen — die
Umsetzung passiert einmal beim Einstellen statt bei jedem Abruf. Der Ort
überlebt jetzt auch den Neustart; vorher stand nach jedem Start wieder der
Aufenthaltsort da.

Die Ablage-Einstellungen sind vom Widget-Reiter hierher gewandert, wo die
anderen Widget-Optionen stehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 14:00:48 +02:00

85 lines
3.7 KiB
Swift

import Foundation
/// Woher die Termine kommen sollen.
public enum CalendarSourceKind: String, CaseIterable, Sendable {
case appleCalendar
case calendarr
case both
public var localizedName: String {
switch self {
case .appleCalendar: String(localized: "calendar.source.apple", bundle: .module)
case .calendarr: String(localized: "calendar.source.calendarr", bundle: .module)
case .both: String(localized: "calendar.source.both", bundle: .module)
}
}
/// Fasst die Zustände mehrerer Quellen zu einem zusammen.
///
/// Solange **eine** Quelle liefert, wird geliefert. Calendarr nie geöffnet
/// zu haben darf den Apple-Kalender nicht mit ausknipsen sonst wäre
/// beide" schlechter als jede einzelne Quelle für sich.
public static func merge(_ states: [CalendarSourceState]) -> CalendarSourceState {
guard !states.isEmpty else { return .ready(.empty) }
let windows = states.compactMap { state -> EventWindow? in
if case .ready(let window) = state { return window }
return nil
}
// Keine einzige Quelle liefert: dann ist der erste Grund die Antwort.
// Irgendeine Begründung ist besser als eine leere Liste, die aussieht,
// als wäre nichts los.
guard !windows.isEmpty else { return states[0] }
return .ready(.merged(windows))
}
}
public extension EventWindow {
/// Führt mehrere Fenster zu einem zusammen.
static func merged(_ windows: [EventWindow]) -> EventWindow {
guard !windows.isEmpty else { return .empty }
guard windows.count > 1 else { return windows[0] }
let events = windows.flatMap(\.events).sorted { $0.start < $1.start }
// Die Abdeckung ist der **Schnitt**, nicht die Vereinigung: ein Tag
// gilt nur als bekannt, wenn ihn jede beteiligte Quelle abdeckt. Deckt
// Calendarr den März nicht ab, ist der März unbekannt auch wenn der
// Apple-Kalender dafür Auskunft geben könnte. Sonst zeigte das Raster
// einen halben März als frei", der in Wahrheit nur halb bekannt ist.
var coverage: DateInterval?
for window in windows {
// `nil` heißt unbegrenzt und schränkt daher nichts ein.
guard let next = window.coverage else { continue }
coverage = coverage.map { current in
let start = max(current.start, next.start)
let end = min(current.end, next.end)
// Ohne Überschneidung bleibt nichts übrig und das ist die
// richtige Antwort, nicht ein beliebiger der beiden Zeiträume.
return DateInterval(start: start, end: max(start, end))
} ?? next
}
// Der älteste Stand begrenzt die Verlässlichkeit des Ganzen. Den
// neueren zu zeigen behauptete eine Aktualität, die nicht alle haben.
let asOf = windows.compactMap(\.asOf).min()
return EventWindow(events: events, coverage: coverage, asOf: asOf)
}
}
public extension CalendarSourceState {
/// Blendet die abgewählten Kalender aus.
///
/// Die Abdeckung bleibt unangetastet: welche Kalender man sehen will, sagt
/// nichts darüber aus, für welchen Zeitraum Daten vorliegen. Ein Tag ohne
/// sichtbare Termine bleibt ein bekannter Tag.
func filteringHidden(_ hidden: Set<String>) -> CalendarSourceState {
guard case .ready(let window) = self, !hidden.isEmpty else { return self }
return .ready(EventWindow(
events: window.events.filter { !hidden.contains($0.calendarID) },
coverage: window.coverage,
asOf: window.asOf))
}
}