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>
85 lines
3.7 KiB
Swift
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))
|
|
}
|
|
}
|