Phase 8: Ablage, AirDrop und Calendarr
Calendarr liefert Termine über den Gruppencontainer. Streng lesend: keine eigene Anmeldung, kein Netz, kein Schreiben zurück. Die Abdeckung ist der Kern und kein Detail. Der Schnappschuss reicht 49 Tage nach vorne, und das steht nirgends in der Datei — es ist zugesagtes Verhalten des Schreibers. Außerhalb liegt keine Information vor, nicht keine Termine; das Monatsraster graut solche Tage aus, statt „frei" zu behaupten. Verlockend wäre, die Abdeckung aus dem frühesten Termin abzuleiten. Das ist falsch, und ein Test hält es fest: mehrtägige Termine ragen ins Fenster hinein und werden mitgeschrieben, obwohl ihr Anfang nicht abgedeckt ist. Bei „beide Quellen" ist die Abdeckung der Schnitt, nicht die Vereinigung. Ein Tag gilt nur als bekannt, wenn ihn jede Quelle abdeckt. Umgekehrt darf ein Ausfall einer Quelle die andere nicht mitreißen: Calendarr nie geöffnet zu haben knipst den Apple-Kalender nicht aus. Die Ablage kopiert, statt zu verweisen. Anhänge aus Mail oder WhatsApp liegen in temporären Verzeichnissen — ein Verweis wäre in Stunden tot, und die Ablage wäre leer, wenn man sie braucht. Aus demselben Grund nimmt die Fläche UTType.item statt public.file-url an: solche Anhänge kommen als Dateiversprechen, und loadFileRepresentation löst beides ein. Die übergebene Datei existiert nur innerhalb des Rückrufs, also wird sie dort sofort gesichert statt später auf dem MainActor. Gelöscht wird in den Papierkorb. Automatisches Aufräumen ist standardmäßig aus — eine Ablage, die ungefragt löscht, ist ein Papierkorb mit Zeitschaltuhr. Nebenbei: „mixer.on" und „network.vpn.active" standen als rohe Schlüssel in der Oberfläche. Label(_:systemImage:) kennt kein Bundle und sucht immer im Hauptbundle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user