Kacheln und Fenster sprechen dieselbe Sprache
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9b600de9a9
commit
edb327cabe
+115
-118
@@ -17,26 +17,13 @@ struct WidgetOptionsView: View {
|
||||
let shelfStore: ShelfStore?
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section("options.calendar") {
|
||||
CalendarOptions(model: calendarModel)
|
||||
}
|
||||
|
||||
Section("options.weather") {
|
||||
WeatherOptions(model: weatherModel)
|
||||
}
|
||||
|
||||
SettingsPage {
|
||||
CalendarOptions(model: calendarModel)
|
||||
WeatherOptions(model: weatherModel)
|
||||
if let shelfStore {
|
||||
Section("options.shelf") {
|
||||
ShelfOptions(store: shelfStore)
|
||||
}
|
||||
ShelfOptions(store: shelfStore)
|
||||
}
|
||||
}
|
||||
.formStyle(.grouped)
|
||||
// Der eigene Untergrund des Formulars weicht ein, zwei Prozent von dem
|
||||
// des Fensters ab. In Dunkelgrau sieht man genau das als Kante.
|
||||
.scrollContentBackground(.hidden)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,82 +31,93 @@ struct WidgetOptionsView: View {
|
||||
|
||||
private struct CalendarOptions: View {
|
||||
@Bindable var model: CalendarModel
|
||||
@State private var calendarrState: CalendarSourceState?
|
||||
|
||||
var body: some View {
|
||||
Picker(selection: $model.kind) {
|
||||
ForEach(CalendarSourceKind.allCases, id: \.self) { kind in
|
||||
Text(kind.localizedName).tag(kind)
|
||||
}
|
||||
} label: {
|
||||
Text("options.calendar.source")
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
|
||||
Picker(selection: $model.showsMonthGrid) {
|
||||
Text("options.calendar.agenda").tag(false)
|
||||
Text("options.calendar.month").tag(true)
|
||||
} label: {
|
||||
Text("options.calendar.display")
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
|
||||
// Welche Kalender mitzählen. Gespeichert werden die **abgewählten**:
|
||||
// kommt ein neuer dazu, ist er automatisch dabei, statt dass man ihn
|
||||
// suchen muss, ohne zu wissen, dass es ihn gibt.
|
||||
if model.availableCalendars.isEmpty {
|
||||
Text("options.calendar.noneFound")
|
||||
.font(.callout).foregroundStyle(.secondary)
|
||||
} else {
|
||||
ForEach(model.availableCalendars) { calendar in
|
||||
Toggle(isOn: Binding(
|
||||
get: { !model.hiddenCalendarIDs.contains(calendar.id) },
|
||||
set: { shown in
|
||||
if shown { model.hiddenCalendarIDs.remove(calendar.id) }
|
||||
else { model.hiddenCalendarIDs.insert(calendar.id) }
|
||||
})) {
|
||||
Label {
|
||||
Text(calendar.title)
|
||||
} icon: {
|
||||
Circle()
|
||||
.fill(Color(hex: calendar.colorHex) ?? .secondary)
|
||||
.frame(width: 9, height: 9)
|
||||
}
|
||||
OnyxFormCard(String(localized: "options.calendar")) {
|
||||
OnyxFormRow(String(localized: "options.calendar.source")) {
|
||||
Picker("", selection: $model.kind) {
|
||||
ForEach(CalendarSourceKind.allCases, id: \.self) { kind in
|
||||
Text(kind.localizedName).tag(kind)
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
.frame(width: 180)
|
||||
}
|
||||
|
||||
OnyxFormRow(String(localized: "options.calendar.display")) {
|
||||
Picker("", selection: $model.showsMonthGrid) {
|
||||
Text("options.calendar.agenda").tag(false)
|
||||
Text("options.calendar.month").tag(true)
|
||||
}
|
||||
.labelsHidden()
|
||||
.frame(width: 180)
|
||||
}
|
||||
}
|
||||
|
||||
OnyxFormCard(String(localized: "options.calendar.calendars"),
|
||||
hint: String(localized: "options.calendar.calendars.hint")) {
|
||||
if model.availableCalendars.isEmpty {
|
||||
OnyxFormRow(String(localized: "options.calendar.noneFound"))
|
||||
} else {
|
||||
ForEach(model.availableCalendars) { calendar in
|
||||
OnyxFormRow(calendar.title) {
|
||||
Circle()
|
||||
.fill(Color(hex: calendar.colorHex) ?? .secondary)
|
||||
.frame(width: 9, height: 9)
|
||||
Toggle("", isOn: Binding(
|
||||
get: { !model.hiddenCalendarIDs.contains(calendar.id) },
|
||||
set: { shown in
|
||||
if shown { model.hiddenCalendarIDs.remove(calendar.id) }
|
||||
else { model.hiddenCalendarIDs.insert(calendar.id) }
|
||||
}))
|
||||
.labelsHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if model.kind != .appleCalendar {
|
||||
CalendarrServerSettings(model: model)
|
||||
CalendarrStatus(state: calendarrState,
|
||||
isLoading: model.isLoading,
|
||||
onRefresh: { model.refresh() })
|
||||
.task(id: model.kind) {
|
||||
guard model.kind != .appleCalendar else { return }
|
||||
let now = Date()
|
||||
calendarrState = await CalendarrSource().load(
|
||||
DateInterval(start: now.addingTimeInterval(-14 * 86_400),
|
||||
end: now.addingTimeInterval(60 * 86_400)))
|
||||
}
|
||||
OnyxFormCard(String(localized: "options.calendar.serverGroup")) {
|
||||
CalendarrServerSettings(model: model)
|
||||
|
||||
// Der Zustand kommt aus dem Modell, nicht aus einem zweiten
|
||||
// eigenen Aufruf. Der las immer den Schnappschuss — auch wenn
|
||||
// längst der Server gefragt wurde — und der Knopf daneben
|
||||
// aktualisierte ihn nie.
|
||||
CalendarrStatus(state: model.calendarrState,
|
||||
usesServer: model.usesCalendarrServer,
|
||||
isLoading: model.isLoading,
|
||||
onRefresh: { model.refresh() })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Was Calendarr gerade liefert. Bleibt das Widget leer, steht hier das Warum.
|
||||
///
|
||||
/// Mit einem Knopf zum Nachsehen. Onyx holt sich nichts — es liest den
|
||||
/// Mit einem Knopf zum Nachsehen. Ohne Zugangsdaten liest Onyx den
|
||||
/// Schnappschuss, den Calendarr schreibt, und erfährt neue über eine
|
||||
/// Darwin-Nachricht. Die ist flüchtig: wer Calendarr gerade erst gestartet hat,
|
||||
/// will nicht auf den nächsten Blick warten, sondern jetzt nachsehen.
|
||||
/// will nicht auf den nächsten Blick warten, sondern jetzt nachsehen. Mit
|
||||
/// Zugangsdaten fragt Onyx den Server selbst — dann sagt diese Zeile, wann das
|
||||
/// zuletzt gelang.
|
||||
private struct CalendarrStatus: View {
|
||||
let state: CalendarSourceState?
|
||||
/// Ob gerade direkt vom Server gelesen wird oder aus dem Schnappschuss.
|
||||
///
|
||||
/// Ohne diese Auskunft ist der Zeitstempel daneben nicht zu deuten: ein
|
||||
/// altes Datum heißt beim Schnappschuss „Calendarr lief lange nicht" und
|
||||
/// beim Server „etwas stimmt nicht".
|
||||
let usesServer: Bool
|
||||
let isLoading: Bool
|
||||
let onRefresh: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
OnyxFormRow(String(localized: usesServer ? "options.calendar.viaServer"
|
||||
: "options.calendar.viaSnapshot"),
|
||||
hint: usesServer ? nil
|
||||
: String(localized: "options.calendar.viaSnapshot.hint")) {
|
||||
message
|
||||
Spacer(minLength: 8)
|
||||
if case .ready(let window) = state {
|
||||
Text(window.asOf?.formatted(date: .abbreviated, time: .shortened) ?? "–")
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -180,44 +178,40 @@ private struct WeatherOptions: View {
|
||||
private var usesCurrentLocation: Bool { model.place.isCurrent }
|
||||
|
||||
var body: some View {
|
||||
Toggle(isOn: Binding(
|
||||
get: { usesCurrentLocation },
|
||||
set: { model.place = $0 ? .current : model.place })) {
|
||||
Text("options.weather.automatic")
|
||||
OnyxFormCard(String(localized: "options.weather")) {
|
||||
OnyxFormRow(String(localized: "options.weather.automatic"),
|
||||
// Ohne Ortungsberechtigung nützt die Automatik nichts —
|
||||
// und das steht hier, statt dass das Widget stumm leer
|
||||
// bleibt.
|
||||
hint: usesCurrentLocation && model.locationIsDenied
|
||||
? String(localized: "options.weather.needsLocation") : nil) {
|
||||
Toggle("", isOn: Binding(
|
||||
get: { usesCurrentLocation },
|
||||
set: { model.place = $0 ? .current : model.place }))
|
||||
.labelsHidden()
|
||||
.disabled(usesCurrentLocation)
|
||||
}
|
||||
.disabled(usesCurrentLocation)
|
||||
|
||||
if usesCurrentLocation {
|
||||
// Ohne Ortungsberechtigung nützt die Automatik nichts — und das
|
||||
// steht hier, statt dass das Widget stumm leer bleibt.
|
||||
if model.locationIsDenied {
|
||||
Text("options.weather.needsLocation")
|
||||
.font(.callout).foregroundStyle(Onyx.Color.warning)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
if case .fixed(let name, _, _) = model.place {
|
||||
OnyxFormRow(String(localized: "options.weather.place")) {
|
||||
Text(name).foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
} else if case .fixed(let name, _, _) = model.place {
|
||||
LabeledContent {
|
||||
Text(name).foregroundStyle(.secondary)
|
||||
} label: {
|
||||
Text("options.weather.place")
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
TextField("options.weather.search", text: $query)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.onSubmit { Task { await search() } }
|
||||
if searching {
|
||||
ProgressView().controlSize(.small)
|
||||
} else {
|
||||
Button("options.weather.set") { Task { await search() } }
|
||||
.disabled(query.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||
OnyxFormRow(String(localized: "options.weather.search"),
|
||||
hint: notFound ? String(localized: "options.weather.notFound") : nil) {
|
||||
TextField("", text: $query)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.frame(width: 160)
|
||||
.onSubmit { Task { await search() } }
|
||||
if searching {
|
||||
ProgressView().controlSize(.small)
|
||||
} else {
|
||||
Button("options.weather.set") { Task { await search() } }
|
||||
.disabled(query.trimmingCharacters(in: .whitespaces).isEmpty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if notFound {
|
||||
Text("options.weather.notFound").font(.callout).foregroundStyle(Onyx.Color.warning)
|
||||
}
|
||||
}
|
||||
|
||||
private func search() async {
|
||||
@@ -241,23 +235,26 @@ private struct ShelfOptions: View {
|
||||
private static let choices: [Int?] = [nil, 1, 7, 30, 90]
|
||||
|
||||
var body: some View {
|
||||
Picker(selection: $store.autoCleanupDays) {
|
||||
ForEach(Self.choices, id: \.self) { days in
|
||||
if let days {
|
||||
Text("settings.shelf.days \(days)").tag(Optional(days))
|
||||
} else {
|
||||
Text("settings.shelf.never").tag(Int?.none)
|
||||
OnyxFormCard(String(localized: "options.shelf")) {
|
||||
OnyxFormRow(String(localized: "settings.shelf.cleanup")) {
|
||||
Picker("", selection: $store.autoCleanupDays) {
|
||||
ForEach(Self.choices, id: \.self) { days in
|
||||
if let days {
|
||||
Text("settings.shelf.days \(days)").tag(Optional(days))
|
||||
} else {
|
||||
Text("settings.shelf.never").tag(Int?.none)
|
||||
}
|
||||
}
|
||||
}
|
||||
.labelsHidden()
|
||||
.frame(width: 180)
|
||||
}
|
||||
} label: {
|
||||
Text("settings.shelf.cleanup")
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("settings.shelf.count \(store.items.count)").foregroundStyle(.secondary)
|
||||
Spacer()
|
||||
Button("settings.shelf.reveal") { store.revealInFinder() }
|
||||
.buttonStyle(.borderless)
|
||||
OnyxFormRow(String(localized: "options.shelf")) {
|
||||
Text("settings.shelf.count \(store.items.count)")
|
||||
.foregroundStyle(.secondary)
|
||||
Button("settings.shelf.reveal") { store.revealInFinder() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -298,7 +295,7 @@ private struct CalendarrServerSettings: View {
|
||||
var body: some View {
|
||||
if let account {
|
||||
LabeledContent {
|
||||
HStack(spacing: 10) {
|
||||
HStack(spacing: Onyx.Space.row) {
|
||||
Text(account.username).foregroundStyle(.secondary)
|
||||
Button {
|
||||
credentials.clear()
|
||||
|
||||
Reference in New Issue
Block a user