Das Grün, das überall auftauchte, war die System-Akzentfarbe: `.tint` und `.accentColor` werden ohne eigene Angabe dorthin durchgereicht, und das betraf alle Popovers und das ganze Einstellungsfenster. Die Notch-Widgets dagegen benutzten Onyx' eigenes Blau. Zwei Farben, ungewollt. Jetzt eine, und die ist einstellbar. Ein neuer Reiter „Farben": Akzentfarbe mit sechs Vorschlägen und einem Knopf, der die Systemfarbe übernimmt, dazu das Paar für Herunter- und Hochladen samt Tauschknopf. Die Signalfarben bleiben fest und stehen nur zur Ansicht dort. Gelb heißt Warnung, Rot heißt kritisch, Grün heißt in Ordnung — bei Temperatur, Akku, Lüfter und Fehlern. Wer sie umstellen kann, kann ihre Bedeutung zerstören; ein rotes „alles gut" liest niemand richtig. Sichtbar sind sie trotzdem, sonst sucht man die Warnfarbe im Reiter und findet sie nirgends. Umgesetzt über die Tokens statt über vierzig Fundstellen: `Onyx.Color.accent` ist keine Konstante mehr, sondern liest bei jedem Zugriff aus `OnyxTheme`. Damit merkt SwiftUI die Abhängigkeit und zeichnet neu, sobald die Farbe sich ändert. Dazu ein `.tint` an den vier Wurzeln — Panel, Popovers, Einstellungen, Einrichtung —, damit auch Haken, Regler und Auswahlfelder mitziehen. Nebenbei die Beschriftung: „Je Programm" heißt jetzt „Programme". Großgesetzt las sich das als englisches „J-E". 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: 4) {
|
||
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: 2) {
|
||
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)))
|
||
}
|
||
}
|