D.10 401-Handling: APIError.unauthorized, NotificationCenter-Bridge, AuthSession.logout() bei 401 → User landet wieder im Login D.12 PWA-Deep-Links: Settings-Section mit Forum/Hunde/Walks/Settings öffnet Safari per https://banyaro.app/#fragment B.4 Auto-Pause: 2-min-Inaktivität → isAutoPaused, automatischer Resume bei nächstem GPS-Update. Settings-Toggle, im UI eigenes Badge "Auto-Pause" (grau vs. Pause orange). C.7 Edit/Delete: RouteUpdateBody + APIClient.patch + APIClient.delete, EditRouteSheet (Name/Beschreibung/Public), Menu in Toolbar (nur eigene Touren), Alert für Delete. C.9 Statistik-Tab: neuer Tab "Statistik" zwischen Hunde und Mehr. Filtert /api/routes auf meine Touren, rechnet Woche/Monat/Allzeit (Distanz, Dauer, Touren), Längste Tour, aktuelle Streak (Tage in Folge). B.5 Walk-Review: Map-Header an die Spitze des FinishWalkSheet-Forms. B.6 Geo-Fotos: CapturedPhoto (Data + GPSPoint?), PhotoLocation @Model in SwiftData. Kamera während Walk taggt mit tracker.points.last. Nach Upload: foto_url aus Response → PhotoLocation persistiert. MiniRouteMap rendert Annotations mit Tap-Callback, PhotoViewerSheet zeigt Foto fullscreen. C.8 Share PNG+GPX: RouteShareImage (MKMapSnapshotter + Polyline overlay + SwiftUI ShareCard via ImageRenderer), GPXExporter (Tempfile mit XML), ShareSheet (UIActivityViewController-Wrapper), Menu in Route-Toolbar. D.11 Icon-Varianten: AppIcon-Dark (0.45 Brightness), AppIcon-Tinted (Grayscale + Kontrastverstärkung), Contents.json mit appearance entries. A.2 HealthKit: BanYaroGo.entitlements (com.apple.developer.healthkit), NSHealthShare/UpdateUsageDescription. WalkHealthSync.shared mit HKWorkoutBuilder (.walking) + HKWorkoutRouteBuilder, Timestamps gleichmäßig über Walk-Dauer verteilt. Settings-Toggle mit Permission-Request.
110 lines
2.5 KiB
Swift
110 lines
2.5 KiB
Swift
import Foundation
|
|
|
|
// MARK: - Auth
|
|
|
|
struct LoginRequest: Encodable {
|
|
let email: String
|
|
let password: String
|
|
}
|
|
|
|
struct LoginResponse: Decodable {
|
|
let token: String
|
|
let name: String
|
|
let isPremium: Bool
|
|
}
|
|
|
|
struct UserProfile: Decodable {
|
|
let id: Int
|
|
let name: String
|
|
let email: String
|
|
let realName: String?
|
|
let rolle: String?
|
|
let isPremium: Bool?
|
|
// Backend bool-konvertiert nur is_premium; is_founder/is_partner kommen
|
|
// als SQLite-Int 0/1 zurück — deshalb hier Int? statt Bool?.
|
|
let isFounder: Int?
|
|
let isPartner: Int?
|
|
let founderNumber: Int?
|
|
let subscriptionTier: String?
|
|
let avatarUrl: String?
|
|
let wohnort: String?
|
|
let bio: String?
|
|
|
|
var isFounderFlag: Bool { isFounder == 1 }
|
|
var isPartnerFlag: Bool { isPartner == 1 }
|
|
}
|
|
|
|
// MARK: - Dogs
|
|
|
|
struct Dog: Decodable, Identifiable {
|
|
let id: Int
|
|
let name: String
|
|
let rasse: String?
|
|
let fotoUrl: String?
|
|
let geburtstag: String?
|
|
}
|
|
|
|
// MARK: - Routes
|
|
|
|
struct GPSPoint: Codable, Hashable {
|
|
let lat: Double
|
|
let lon: Double
|
|
let alt: Double?
|
|
}
|
|
|
|
struct RouteListItem: Decodable, Identifiable {
|
|
let id: Int
|
|
let userId: Int
|
|
let name: String
|
|
let beschreibung: String?
|
|
let distanzKm: Double?
|
|
let dauerMin: Int?
|
|
let createdAt: String?
|
|
let previewTrack: [GPSPoint]
|
|
let fotoUrls: [String]?
|
|
let userName: String?
|
|
let isPublic: Bool?
|
|
}
|
|
|
|
struct RouteDetail: Decodable, Identifiable {
|
|
let id: Int
|
|
let userId: Int
|
|
let name: String
|
|
let beschreibung: String?
|
|
let distanzKm: Double?
|
|
let dauerMin: Int?
|
|
let gpsTrack: [GPSPoint]
|
|
let fotoUrls: [String]?
|
|
let createdAt: String?
|
|
let userName: String?
|
|
let dogIds: [Int]?
|
|
}
|
|
|
|
struct RouteCreateBody: Encodable {
|
|
let name: String
|
|
let gpsTrack: [GPSPoint]
|
|
let distanzKm: Double
|
|
let dauerMin: Int
|
|
let dogIds: [Int]
|
|
let isPublic: Bool
|
|
}
|
|
|
|
/// Patch body for PATCH /api/routes/{id}. Only non-nil fields are encoded.
|
|
struct RouteUpdateBody: Encodable {
|
|
var name: String?
|
|
var beschreibung: String?
|
|
var isPublic: Bool?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case name
|
|
case beschreibung
|
|
case isPublic
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var c = encoder.container(keyedBy: CodingKeys.self)
|
|
try c.encodeIfPresent(name, forKey: .name)
|
|
try c.encodeIfPresent(beschreibung, forKey: .beschreibung)
|
|
try c.encodeIfPresent(isPublic, forKey: .isPublic)
|
|
}
|
|
}
|