Files
certain/ios/Bausteine/Darstellung.swift
T
ScarriffleandClaude Opus 5 6b86a6bf91 feat: Erste Stufe der iOS-App - Anmeldung und Kontenliste
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>
2026-10-20 16:20:03 +02:00

110 lines
4.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}