1.1: Offline-Cache + Outbox für Touren/Tagebuch, WeatherKit-Fix, Aufräumen

App-Review-Fix (Guideline 2.1 WeatherKit):
- OneShotLocation: deterministisches async resolve() mit 10s-Timeout statt
  onChange-Lauschen; WetterView lädt bei fehlendem Standort einen Berlin-Fallback
  → kein ewiges Hängen bei "Hole Standort…", WeatherKit ist immer sichtbar.

Offline-Lesen (SwiftData):
- CachedRoute/CachedDiaryEntry/CachedImage + CachedAsyncImage: Touren, Tagebuch
  und Fotos werden cache-first geladen und sind offline verfügbar.
- Cache wird bei Logout/401 geleert (RootView), kein Durchschimmern fremder User.

Offline-Speichern (Outbox):
- PendingRoute/PendingRoutePhoto: Tour inkl. unterwegs hinzugefügter Fotos wird
  offline lokal gesichert und automatisch hochgeladen (Touren-Tab + App-Start).
- Touren-Liste zeigt offline gesicherte Touren mit "wird hochgeladen"-Badge.

FinishWalkSheet:
- Dismiss-Schutz: Speichern-Dialog lässt sich nicht mehr wegwischen — eine
  aufgezeichnete Tour geht nicht mehr durch Runterwischen verloren.

Wetter:
- Ortslabel (Reverse-Geocoding; Fallback "Berlin · Näherung").
- Saubere Offline-Meldung statt rohem networkError.

Aufräumen:
- Doppeltes "Gassi-Treffen" im Mehr-Tab entfernt.
- Veraltete Phase-1/2-Texte neu getextet.
- Tote DogsListView gelöscht (Hund-Wechsel läuft über den Heim-Picker).
This commit is contained in:
rene 2026-06-02 19:37:30 +02:00
parent 9e51f3910e
commit a2646a18ef
16 changed files with 769 additions and 199 deletions

View file

@ -1,7 +1,9 @@
import SwiftUI
import SwiftData
struct TagebuchView: View {
@Environment(ActiveDogStore.self) private var activeDog
@Environment(\.modelContext) private var ctx
@State private var entries: [DiaryEntry] = []
@State private var isLoading = false
@ -66,16 +68,26 @@ struct TagebuchView: View {
private func load() async {
guard let dog = activeDog.activeDog else { return }
isLoading = true
// 1) Cache-first: sofort anzeigen (auch offline)
if entries.isEmpty {
let cached = OfflineCache.cachedDiary(dogId: dog.id, in: ctx)
if !cached.isEmpty { entries = cached }
}
isLoading = entries.isEmpty
errorMessage = nil
defer { isLoading = false }
// 2) Netzwerk Cache aktualisieren; bei Fehler bleibt der Cache stehen
do {
entries = try await APIClient.shared.get("/api/dogs/\(dog.id)/diary?limit=50")
let fetched: [DiaryEntry] = try await APIClient.shared.get("/api/dogs/\(dog.id)/diary?limit=50")
entries = fetched
OfflineCache.upsertDiary(fetched, in: ctx)
let paths = fetched.flatMap { ($0.mediaItems ?? []).map(\.url) }
Task { await OfflineCache.prefetchImages(paths: paths, in: ctx) }
} catch let decodingError as DecodingError {
errorMessage = Self.describe(decodingError)
if entries.isEmpty { errorMessage = Self.describe(decodingError) }
print("Tagebuch decode error: \(decodingError)")
} catch {
errorMessage = error.localizedDescription
if entries.isEmpty { errorMessage = error.localizedDescription }
}
}
@ -131,11 +143,10 @@ private struct DiaryRow: View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 6) {
ForEach(media) { m in
AsyncImage(url: URL(string: "https://banyaro.app\(m.url)")) { phase in
switch phase {
case .success(let img): img.resizable().scaledToFill()
default: Rectangle().fill(.gray.opacity(0.15))
}
CachedAsyncImage(path: m.url) { img in
img.resizable().scaledToFill()
} placeholder: {
Rectangle().fill(.gray.opacity(0.15))
}
.frame(width: 80, height: 80)
.clipShape(RoundedRectangle(cornerRadius: 8))