import SwiftUI /// Einstellungen und die Nebenräume: Kategorien, Schlagwörter, Bank, /// zweiter Faktor. /// /// Sie hängen hier und nicht als eigene Reiter: Man braucht sie selten, und /// eine Leiste mit zehn Reitern findet niemand mehr. struct EinstellungenAnsicht: View { @Environment(AppZustand.self) private var zustand @State private var vorgang = Speichervorgang() @State private var zeigeAbmeldefrage = false @State private var markierte: [MarkiertesWort] = [] @State private var aenderePIN = false var body: some View { Form { Section { if case .bereit(let benutzer) = zustand.lage { LabeledContent(String(localized: "einstellungen.angemeldetAls"), value: benutzer.anzeigename) } if let adresse = zustand.serverAdresse { LabeledContent(String(localized: "einstellungen.server"), value: adresse.host() ?? adresse.absoluteString) } NavigationLink { ZweiterFaktorAnsicht() } label: { Label(String(localized: "zweifaktor.titel"), systemImage: "lock.shield") } Button { aenderePIN = true } label: { Label(String(localized: "pin.aendern"), systemImage: "lock.rotation") } } header: { Text(String(localized: "einstellungen.zugang")) } footer: { Text(String(localized: "einstellungen.markeErklaerung")) } Section(String(localized: "nebenraeume.titel")) { NavigationLink { KategorienAnsicht() } label: { Label(String(localized: "nebenraeume.kategorien"), systemImage: "square.grid.2x2") } NavigationLink { TagsAnsicht() } label: { Label(String(localized: "nebenraeume.tags"), systemImage: "tag") } NavigationLink { BankAnsicht() } label: { Label(String(localized: "nebenraeume.bank"), systemImage: "building.columns") } } if !markierte.isEmpty { Section { ForEach(markierte) { wort in MarkiertesWortZeile(wort: wort, behalten: { Task { await behalte(wort) } }, entfernen: { Task { await entferne(wort) } }) } } header: { Text(String(localized: "kategorien.markiertTitel")) } footer: { Text(String(localized: "kategorien.markiertErklaerung")) } } Section { Picker(String(localized: "einstellungen.erscheinungsbild"), selection: bindung(\.erscheinungsbild, "theme")) { Text(String(localized: "einstellungen.system")).tag("system") Text(String(localized: "einstellungen.hell")).tag("light") Text(String(localized: "einstellungen.dunkel")).tag("dark") } Picker(String(localized: "einstellungen.hauptwaehrung"), selection: bindung(\.basiswaehrung, "base_currency")) { ForEach(zustand.einstellungen.aktiveWaehrungen, id: \.self) { code in Text(code).tag(code) } } Picker(String(localized: "einstellungen.budgetStarttag"), selection: tagBindung) { ForEach(1...28, id: \.self) { tag in Text("\(tag).").tag(tag) } } } header: { Text(String(localized: "einstellungen.ausDemServer")) } footer: { // Die Hauptwährung rechnet alle Auswertungen um. Das gilt // für jedes Gerät, nicht nur für dieses. Text(String(localized: "einstellungen.serverweitErklaerung")) } if let fehler = vorgang.fehler { Section { FehlerZeile(text: fehler) } } Section { Button(String(localized: "allgemein.abmelden"), role: .destructive) { zeigeAbmeldefrage = true } } footer: { Text(String(localized: "einstellungen.abmeldenErklaerung")) } } .sheet(isPresented: $aenderePIN) { PINEinrichtenAnsicht(pflicht: false) } .navigationTitle(String(localized: "reiter.einstellungen")) .refreshable { await lade() } .task { await lade() } .confirmationDialog(String(localized: "einstellungen.abmeldenFrage"), isPresented: $zeigeAbmeldefrage, titleVisibility: .visible) { Button(String(localized: "allgemein.abmelden"), role: .destructive) { Task { await zustand.meldeAb() } } Button(String(localized: "allgemein.abbrechen"), role: .cancel) {} } } // MARK: - Einstellungen schreiben /// Eine Bindung, die beim Setzen gleich zum Server geht. /// /// Ein „Sichern"-Knopf wäre hier falsch: Es ist je Zeile genau ein Wert, /// und das Backend nimmt ihn einzeln entgegen (`PUT /settings/{key}`). private func bindung(_ pfad: KeyPath, _ schluessel: String) -> Binding { Binding(get: { zustand.einstellungen[keyPath: pfad] }, set: { neu in Task { await setze(schluessel, neu) } }) } private var tagBindung: Binding { Binding(get: { zustand.einstellungen.budgetStarttag }, set: { neu in Task { await setze("budget_start_day", String(neu)) } }) } private func setze(_ schluessel: String, _ wert: String) async { guard let client = zustand.client else { return } let geglueckt = await vorgang.fuehreAus { _ = try await client.setzeEinstellung(schluessel, wert) } if geglueckt { await zustand.aktualisiereEinstellungen() } } // MARK: - Markierte Erkennungswörter private func lade() async { await zustand.aktualisiereEinstellungen() guard let client = zustand.client else { return } markierte = (try? await client.markierteErkennungswoerter()) ?? [] } private func behalte(_ wort: MarkiertesWort) async { guard let client = zustand.client else { return } if await vorgang.fuehreAus({ try await client.bestaetigeErkennungswort(wort.id) }) { await lade() } } private func entferne(_ wort: MarkiertesWort) async { guard let client = zustand.client else { return } if await vorgang.fuehreAus({ try await client.loescheErkennungswort(wort.id) }) { await lade() await zustand.ladeNachschlag() } } } /// Ein Erkennungswort, das oft wieder korrigiert wird. private struct MarkiertesWortZeile: View { let wort: MarkiertesWort let behalten: () -> Void let entfernen: () -> Void var body: some View { VStack(alignment: .leading, spacing: 6) { HStack { Text(wort.word) .font(.callout) Spacer() Text(wort.categoryName) .font(.caption2) .foregroundStyle(.secondary) } Text(String(format: String(localized: "kategorien.markiertZaehler"), wort.appliedCount, wort.overriddenCount)) .font(.caption2) .foregroundStyle(.secondary) HStack(spacing: 8) { Button(String(localized: "kategorien.behalten"), action: behalten) .buttonStyle(.bordered) .controlSize(.small) Button(String(localized: "kategorien.entfernen"), role: .destructive, action: entfernen) .buttonStyle(.bordered) .controlSize(.small) } } .padding(.vertical, 2) } }