Neuer Ordner ios/ mit dem Geruest: Netzwerkschicht, Modelle, Offline-Ablage, Texte in Deutsch und Englisch, Navigation, der komplette Anmeldeweg (Server waehlen, anmelden, zweiter Faktor, Pflicht-Passwortwechsel) und als erste Fachansicht die Kontenliste. Die Anmeldung laeuft einmal ueber das Sitzungs-Cookie, holt sich dabei eine App-Marke und legt sie in den Schluesselbund. Danach traegt nur noch der Header. Kein Passwort auf dem Geraet, kein Ablauf nach 30 Tagen. Der Code ist nie uebersetzt worden - hier gibt es kein Xcode. Geprueft ist dafuer jedes Modellfeld gegen echte Antworten des laufenden Backends; dabei kam heraus, dass created_at ohne Zeitzone und mit Mikrosekunden kommt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
110 lines
4.8 KiB
Swift
110 lines
4.8 KiB
Swift
import SwiftUI
|
||
|
||
extension Color {
|
||
/// Die Farben aus der Datenbank sind Hex-Zeichenketten wie `#3f51b5`.
|
||
/// Alles, was nicht passt, wird zum neutralen Grau – eine kaputte Farbe
|
||
/// darf keine Ansicht leer lassen.
|
||
init(hex: String) {
|
||
var text = hex.trimmingCharacters(in: .whitespacesAndNewlines)
|
||
if text.hasPrefix("#") { text.removeFirst() }
|
||
// Das Backend erlaubt acht Stellen (#rrggbbaa); die Durchsicht nehmen
|
||
// wir mit, der Rest wird ignoriert.
|
||
guard text.count == 6 || text.count == 8,
|
||
let zahl = UInt64(text, radix: 16) else {
|
||
self = Color(red: 0.42, green: 0.45, blue: 0.50)
|
||
return
|
||
}
|
||
let hatAlpha = text.count == 8
|
||
let r = Double((zahl >> (hatAlpha ? 24 : 16)) & 0xFF) / 255
|
||
let g = Double((zahl >> (hatAlpha ? 16 : 8)) & 0xFF) / 255
|
||
let b = Double((zahl >> (hatAlpha ? 8 : 0)) & 0xFF) / 255
|
||
let a = hatAlpha ? Double(zahl & 0xFF) / 255 : 1
|
||
self = Color(.sRGB, red: r, green: g, blue: b, opacity: a)
|
||
}
|
||
}
|
||
|
||
/// Übersetzt die Symbolnamen aus der Datenbank in SF Symbols.
|
||
///
|
||
/// Die Weboberfläche benutzt Lucide-Namen und hält dafür ein eigenes
|
||
/// SVG-Verzeichnis (`web/src/components/Icon.jsx`, knapp 600 Zeilen). Nativ
|
||
/// wäre das Unsinn: iOS bringt die Symbole mit, sie passen zur Systemschrift
|
||
/// und können fett, farbig und in jeder Grösse. Nur die Namen muss jemand
|
||
/// zuordnen – das ist diese Tabelle.
|
||
///
|
||
/// Die Liste deckt `ACCOUNT_ICONS` und `CATEGORY_ICONS` aus
|
||
/// `web/src/components/IconPicker.jsx` ab. Unbekanntes wird zum Platzhalter,
|
||
/// nie zu einer leeren Stelle.
|
||
enum Symbole {
|
||
static let platzhalter = "circle.dashed"
|
||
|
||
private static let tabelle: [String: String] = [
|
||
// Konten
|
||
"wallet": "wallet.bifold", "landmark": "building.columns",
|
||
"piggy-bank": "banknote", "vault": "lock.square.stack",
|
||
"credit-card": "creditcard", "trending-up": "chart.line.uptrend.xyaxis",
|
||
"banknote": "banknote", "coins": "centsign.circle",
|
||
// Wohnen
|
||
"home": "house", "bed": "bed.double", "zap": "bolt",
|
||
"droplet": "drop", "flame": "flame", "wifi": "wifi",
|
||
"monitor": "display", "cloud": "cloud",
|
||
// Alltag
|
||
"shopping-cart": "cart", "shopping-bag": "bag", "store": "storefront",
|
||
"utensils": "fork.knife", "coffee": "cup.and.saucer",
|
||
"wine": "wineglass", "shirt": "tshirt", "scissors": "scissors",
|
||
"sparkle": "sparkles",
|
||
// Unterwegs
|
||
"car": "car", "fuel": "fuelpump", "bus": "bus", "bike": "bicycle",
|
||
"plane": "airplane", "ticket": "ticket",
|
||
// Gesundheit
|
||
"heart-pulse": "heart.text.square", "pill": "pills",
|
||
"dumbbell": "figure.strengthtraining.traditional",
|
||
// Freizeit
|
||
"film": "film", "music": "music.note", "book-open": "book",
|
||
"newspaper": "newspaper", "graduation-cap": "graduationcap",
|
||
"paw-print": "pawprint",
|
||
// Familie
|
||
"sprout": "leaf", "cake": "birthday.cake", "gift": "gift",
|
||
"baby": "figure.and.child.holdinghands", "heart": "heart",
|
||
// Vorsorge
|
||
"umbrella": "umbrella", "life-buoy": "lifepreserver",
|
||
"shield": "shield", "scale": "scalemass",
|
||
// Sonstiges
|
||
"briefcase": "briefcase", "wrench": "wrench.adjustable",
|
||
"hammer": "hammer", "phone": "phone", "refresh-ccw": "arrow.triangle.2.circlepath",
|
||
"percent": "percent", "receipt": "receipt",
|
||
"list": "list.bullet", "more-horizontal": "ellipsis",
|
||
// In der Oberfläche benutzt, nicht in der Auswahl
|
||
"tags": "tag", "inbox": "tray", "settings": "gearshape",
|
||
"arrow-left-right": "arrow.left.arrow.right", "pie-chart": "chart.pie",
|
||
"dashboard": "square.grid.2x2", "download": "square.and.arrow.down",
|
||
"calendar": "calendar", "search": "magnifyingglass",
|
||
"edit": "pencil", "trash": "trash", "plus": "plus", "check": "checkmark",
|
||
"close": "xmark", "chevron-down": "chevron.down",
|
||
]
|
||
|
||
static func sf(_ name: String?) -> String {
|
||
guard let name, let treffer = tabelle[name] else { return platzhalter }
|
||
return treffer
|
||
}
|
||
}
|
||
|
||
/// Ein farbiges Symbolplättchen – das native Gegenstück zum `IconChip` im Web.
|
||
struct Symbolplakette: View {
|
||
let symbol: String?
|
||
let farbe: String
|
||
var groesse: CGFloat = 34
|
||
|
||
var body: some View {
|
||
let farbwert = Color(hex: farbe)
|
||
RoundedRectangle(cornerRadius: groesse * 0.28, style: .continuous)
|
||
.fill(farbwert.opacity(0.16))
|
||
.frame(width: groesse, height: groesse)
|
||
.overlay {
|
||
Image(systemName: Symbole.sf(symbol))
|
||
.font(.system(size: groesse * 0.46, weight: .medium))
|
||
.foregroundStyle(farbwert)
|
||
}
|
||
.accessibilityHidden(true)
|
||
}
|
||
}
|