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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8a7545bf5b
commit
6b86a6bf91
@@ -0,0 +1,109 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user