Alle Kacheln, Popovers und Fenster auf die Designschicht umgestellt: rund hundert gesetzte Schriftgrößen und 159 gesetzte Abstände und Radien sind jetzt Token. Übrig bleibt `spacing: 0` — das ist eine Aussage, keine geratene Zahl. Die Einstellungen bekommen eine Seitenleiste statt sieben Reitern, deren deutsche Beschriftungen zusammen 880 Punkte brauchten. Panel-Token stehen nicht mehr in Fenstern: dort gilt Systemsemantik, damit der Hellmodus nicht schwarz auf schwarz endet. Neu ist ein Schaufenster, das alle Bauteile nebeneinander zeigt — ohne das lässt sich eine Änderung an einem Token nur prüfen, indem man die App bedient, bis jede Kachel einmal zu sehen war. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
108 lines
4.2 KiB
Swift
108 lines
4.2 KiB
Swift
import SwiftUI
|
||
import OnyxDesign
|
||
|
||
/// Die Wahl der Terminquelle — samt ehrlicher Auskunft darüber, was Calendarr
|
||
/// gerade liefert.
|
||
///
|
||
/// Der Zustandstext ist kein Beiwerk: bleibt das Widget leer, ist genau hier
|
||
/// die Antwort auf das Warum. Ohne ihn sieht „Calendarr nie geöffnet" genauso
|
||
/// aus wie „keine Termine".
|
||
public struct CalendarSourceSettings: View {
|
||
|
||
@Bindable private var model: CalendarModel
|
||
@State private var calendarrState: CalendarSourceState?
|
||
|
||
public init(model: CalendarModel) {
|
||
self.model = model
|
||
}
|
||
|
||
public var body: some View {
|
||
Form {
|
||
Section {
|
||
Picker(selection: $model.kind) {
|
||
ForEach(CalendarSourceKind.allCases, id: \.self) { kind in
|
||
Text(kind.localizedName).tag(kind)
|
||
}
|
||
} label: {
|
||
Text("calendar.source.title", bundle: .module)
|
||
}
|
||
.pickerStyle(.radioGroup)
|
||
}
|
||
|
||
if model.kind != .appleCalendar {
|
||
Section {
|
||
LabeledContent {
|
||
VStack(alignment: .leading, spacing: Onyx.Space.xs) {
|
||
statusText
|
||
Text("calendar.source.readOnly", bundle: .module)
|
||
.font(.caption)
|
||
.foregroundStyle(.tertiary)
|
||
.fixedSize(horizontal: false, vertical: true)
|
||
}
|
||
} label: {
|
||
Text(verbatim: "Calendarr")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
.formStyle(.grouped)
|
||
.task(id: model.kind) { await loadCalendarrState() }
|
||
}
|
||
|
||
@ViewBuilder
|
||
private var statusText: some View {
|
||
switch calendarrState {
|
||
case .ready(let window):
|
||
VStack(alignment: .leading, spacing: Onyx.Space.xxs) {
|
||
Text("calendar.source.status.ready \(window.events.count.formatted()) \(asOfText(window.asOf))",
|
||
bundle: .module)
|
||
if let coverage = window.coverage {
|
||
// Die Grenze der Abdeckung gehört sichtbar hin: dahinter
|
||
// zeigt Onyx bewusst „unbekannt" statt „frei", und das
|
||
// soll niemand für einen Fehler halten.
|
||
Text("calendar.source.status.coverage \(coverage.end.formatted(date: .abbreviated, time: .omitted))",
|
||
bundle: .module)
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
.font(.callout)
|
||
case .neverWritten:
|
||
notice("calendar.source.status.neverWritten")
|
||
case .loggedOut:
|
||
notice("calendar.source.status.loggedOut")
|
||
case .incompatible:
|
||
notice("calendar.source.status.incompatible")
|
||
case .unreadable(let reason):
|
||
Text("calendar.source.status.unreadable \(reason)", bundle: .module)
|
||
.font(.callout).foregroundStyle(Onyx.Color.warning)
|
||
.fixedSize(horizontal: false, vertical: true)
|
||
case .denied, .none:
|
||
ProgressView().controlSize(.small)
|
||
}
|
||
}
|
||
|
||
private func notice(_ key: String.LocalizationValue) -> some View {
|
||
Text(String(localized: key, bundle: .module))
|
||
.font(.callout)
|
||
.foregroundStyle(.secondary)
|
||
.fixedSize(horizontal: false, vertical: true)
|
||
}
|
||
|
||
private func asOfText(_ date: Date?) -> String {
|
||
date?.formatted(date: .abbreviated, time: .shortened) ?? "–"
|
||
}
|
||
|
||
private func loadCalendarrState() async {
|
||
guard model.kind != .appleCalendar else { return }
|
||
// Bewusst über die Quelle selbst und nicht über das Modell: hier soll
|
||
// der Zustand von Calendarr allein stehen, nicht die Mischung aus
|
||
// beiden Quellen. Sonst verdeckt ein funktionierender Apple-Kalender,
|
||
// dass Calendarr gar nichts liefert.
|
||
let source = CalendarrSource()
|
||
let now = Date()
|
||
calendarrState = await source.load(
|
||
DateInterval(start: now.addingTimeInterval(-14 * 86_400),
|
||
end: now.addingTimeInterval(60 * 86_400)))
|
||
}
|
||
}
|