banyaro-ios/BanYaroGo/Views/SettingsView.swift
rene f054b2a07f Tagebuch + Heim-Tab mit täglichem Background
Tagebuch (Diary):
- DiaryEntry + DiaryMedia + DiaryCreateBody DTOs
- TagebuchView: Liste der Einträge für aktiven Hund mit Titel, Text,
  Ortsname, Meilenstein-Stern, Foto-Strip
- AddDiaryEntrySheet: Titel/Text/Datum/Meilenstein/Ort/Tags +
  PhotosPicker, nach POST /api/dogs/{id}/diary werden Fotos einzeln
  via POST /api/dogs/{id}/diary/{entry_id}/media hochgeladen (mit
  ImageResize.resizedJPEG)

Heim-Tab als neuer 1. Tab:
- DashboardSnapshot DTO für /api/dogs/{id}/welcome-dashboard
- ActiveDogStore (@Observable + UserDefaults("activeDogId")): hält
  den aktiven Hund app-weit
- HeimView: tägliches Hintergrundfoto aus random_photo.url (rotiert
  pro Tag, vom Backend gewählt), Gradient zur Lesbarkeit, Tagezeit-
  Begrüßung mit User-Namen, Hund-Picker (Menu), Info-Karten für
  letzten Eintrag/nächsten Termin/Gewicht/Eintragszahl,
  Quick-Action-Buttons (Tagebuch, Wetter, Erste Hilfe)

Reorganisation:
- 5 Tabs: Heim, Touren, Aufnehmen, Statistik, Mehr
- Hunde-Liste wandert in Mehr → "Hund & Alltag"
- Tagebuch in Mehr → "Hund & Alltag" + erreichbar von Heim
2026-05-30 12:22:51 +02:00

250 lines
10 KiB
Swift

import SwiftUI
struct SettingsView: View {
@Environment(AuthSession.self) private var auth
@AppStorage("autoPauseEnabled") private var autoPauseEnabled = true
@AppStorage("healthKitSyncEnabled") private var healthKitSyncEnabled = false
@State private var showHealthPermissionAlert = false
var body: some View {
NavigationStack {
Form {
Section {
HStack(spacing: 14) {
avatarView
.frame(width: 60, height: 60)
.clipShape(Circle())
VStack(alignment: .leading, spacing: 2) {
Text(displayName)
.font(.headline)
if let email = auth.profile?.email {
Text(email)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
.padding(.vertical, 4)
}
Section("Hund & Alltag") {
NavigationLink {
DogsListView()
} label: {
Label("Meine Hunde", systemImage: "pawprint.fill")
}
NavigationLink {
TagebuchView()
} label: {
Label("Tagebuch", systemImage: "book.fill")
}
NavigationLink {
ErsteHilfeView()
} label: {
Label("Erste Hilfe", systemImage: "cross.case.fill")
}
NavigationLink {
WetterView()
} label: {
Label("Wetter", systemImage: "cloud.sun.fill")
}
NavigationLink {
GassiZeitenView()
} label: {
Label("Gassi-Zeiten", systemImage: "alarm.fill")
}
NavigationLink {
GiftkoederView()
} label: {
Label("Giftköder", systemImage: "exclamationmark.octagon.fill")
}
NavigationLink {
VerloreneHundeView()
} label: {
Label("Verlorene Hunde", systemImage: "magnifyingglass.circle.fill")
}
NavigationLink {
AusgabenView()
} label: {
Label("Ausgaben", systemImage: "eurosign.circle.fill")
}
}
Section("Account") {
LabeledContent("Rolle", value: rolleLabel)
if auth.profile?.isFounderFlag == true {
LabeledContent("Founder", value: founderLabel)
}
if auth.profile?.isPartnerFlag == true {
LabeledContent("Partner", value: "Ja")
}
if let tier = auth.profile?.subscriptionTier, !tier.isEmpty {
LabeledContent("Abo", value: tier.capitalized)
}
LabeledContent("Premium", value: premiumValue ? "Ja" : "Nein")
}
if let wohnort = auth.profile?.wohnort, !wohnort.isEmpty {
Section("Ort") {
Text(wohnort)
}
}
Section {
Toggle(isOn: $autoPauseEnabled) {
Label("Auto-Pause", systemImage: "pause.circle")
}
Toggle(isOn: $healthKitSyncEnabled) {
Label("Apple Health Sync", systemImage: "heart.fill")
}
.onChange(of: healthKitSyncEnabled) { _, newValue in
if newValue {
Task {
let granted = await WalkHealthSync.shared.requestAuthorization()
if !granted {
healthKitSyncEnabled = false
showHealthPermissionAlert = true
}
}
}
}
} header: {
Text("Aufnahme")
} footer: {
Text("Auto-Pause: pausiert die Aufnahme, wenn du 2 Minuten lang stehen bleibst.\nApple Health: schreibt jede gespeicherte Tour als Spaziergang-Workout mit Route in Health.")
}
Section("Mehr auf banyaro.app") {
pwaLink("Forum", systemImage: "bubble.left.and.bubble.right.fill", fragment: "forum")
pwaLink("Hunde-Profile bearbeiten", systemImage: "pawprint.fill", fragment: "dogs")
pwaLink("Gassi-Treffen", systemImage: "person.2.fill", fragment: "walks")
pwaLink("Profil & Einstellungen", systemImage: "gearshape.fill", fragment: "settings")
}
Section {
if let url = URL(string: "https://banyaro.app") {
Link(destination: url) {
Label("banyaro.app in Safari öffnen", systemImage: "safari.fill")
.foregroundStyle(.primary)
}
}
} header: {
Text("banyaro.app als Web-App")
} footer: {
VStack(alignment: .leading, spacing: 8) {
Text("Du kannst banyaro.app zusätzlich als Web-App auf deinem Home-Bildschirm ablegen — praktisch für alle Features, die diese App nicht abbildet.")
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text("**1.**")
Text("Oben „in Safari öffnen“ tippen")
}
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text("**2.**")
HStack(spacing: 4) {
Text("In Safari unten das Teilen-Icon")
Image(systemName: "square.and.arrow.up")
Text("antippen")
}
}
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text("**3.**")
HStack(spacing: 4) {
Text("„Zum Home-Bildschirm“")
Image(systemName: "plus.square")
Text("auswählen")
}
}
Text("Eine native App kann eine Web-App leider nicht selbst installieren — Apple lässt das nur über Safari zu.")
.padding(.top, 4)
.foregroundStyle(.tertiary)
}
}
Section {
Button("Abmelden", role: .destructive) {
auth.logout()
}
}
Section("Über") {
Text("Ban Yaro Go ist die native iOS-Ergänzung zur banyaro.app PWA. Phase 1: deine Touren ansehen.")
.font(.footnote)
.foregroundStyle(.secondary)
}
}
.navigationTitle("Mehr")
.refreshable { await auth.loadProfile() }
.alert("Apple Health hat den Zugriff verweigert", isPresented: $showHealthPermissionAlert) {
Button("OK", role: .cancel) {}
} message: {
Text("Du kannst die Berechtigung in den iOS-Einstellungen unter Datenschutz & Sicherheit → Health → Ban Yaro Go nachträglich ändern.")
}
}
}
private var displayName: String {
auth.profile?.name ?? auth.userName ?? ""
}
private var premiumValue: Bool {
auth.profile?.isPremium ?? auth.isPremium
}
private var rolleLabel: String {
switch auth.profile?.rolle?.lowercased() {
case "admin": return "Admin"
case "moderator": return "Moderator"
case nil: return ""
default: return "Mitglied"
}
}
private var founderLabel: String {
if let n = auth.profile?.founderNumber { return "#\(n)" }
return "Ja"
}
@ViewBuilder
private var avatarView: some View {
if let path = auth.profile?.avatarUrl, !path.isEmpty,
let url = avatarURL(path) {
AsyncImage(url: url) { phase in
switch phase {
case .success(let img): img.resizable().scaledToFill()
default: avatarPlaceholder
}
}
} else {
avatarPlaceholder
}
}
private var avatarPlaceholder: some View {
ZStack {
Color.accentColor.opacity(0.2)
Image(systemName: "person.crop.circle.fill")
.font(.system(size: 36))
.foregroundStyle(Color.accentColor)
}
}
private func avatarURL(_ path: String) -> URL? {
if path.hasPrefix("http") { return URL(string: path) }
return URL(string: "https://banyaro.app\(path)")
}
@ViewBuilder
private func pwaLink(_ title: String, systemImage: String, fragment: String) -> some View {
if let url = URL(string: "https://banyaro.app/#\(fragment)") {
Link(destination: url) {
HStack {
Label(title, systemImage: systemImage)
.foregroundStyle(.primary)
Spacer()
Image(systemName: "arrow.up.right.square")
.font(.caption)
.foregroundStyle(.tertiary)
}
}
}
}
}