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

@ -8,6 +8,7 @@ struct RouteDetailView: View {
@Environment(\.dismiss) private var dismiss
@Environment(AuthSession.self) private var auth
@Environment(\.modelContext) private var ctx
@Query private var allPhotoLocations: [PhotoLocation]
@State private var detail: RouteDetail?
@ -218,27 +219,32 @@ struct RouteDetailView: View {
}
private func photoThumb(_ path: String) -> some View {
let url = URL(string: "https://banyaro.app\(path)")
return AsyncImage(url: url) { phase in
switch phase {
case .success(let img):
img.resizable().scaledToFill()
default:
Rectangle().fill(.gray.opacity(0.15))
}
CachedAsyncImage(path: path) { img in
img.resizable().scaledToFill()
} placeholder: {
Rectangle().fill(.gray.opacity(0.15))
}
.frame(width: 160, height: 160)
.clipShape(RoundedRectangle(cornerRadius: 10))
}
private func load() async {
isLoading = true
// 1) Cache-first: gespeicherten Detail-Stand zeigen (auch offline)
if detail == nil, let cached = OfflineCache.cachedRouteDetail(id: routeId, in: ctx) {
detail = cached
}
isLoading = (detail == nil)
errorMessage = nil
defer { isLoading = false }
// 2) Netzwerk Cache aktualisieren; bei Fehler bleibt der Cache stehen
do {
detail = try await APIClient.shared.get("/api/routes/\(routeId)")
let fetched: RouteDetail = try await APIClient.shared.get("/api/routes/\(routeId)")
detail = fetched
OfflineCache.upsertRouteDetail(fetched, in: ctx)
let paths = fetched.fotoUrls ?? []
Task { await OfflineCache.prefetchImages(paths: paths, in: ctx) }
} catch {
errorMessage = error.localizedDescription
if detail == nil { errorMessage = error.localizedDescription }
}
}
@ -307,17 +313,11 @@ private struct PhotoViewerSheet: View {
NavigationStack {
ZStack {
Color.black.ignoresSafeArea()
if let url = URL(string: "https://banyaro.app\(path)") {
AsyncImage(url: url) { phase in
switch phase {
case .success(let img):
img.resizable().scaledToFit()
case .failure:
ContentUnavailableView("Foto nicht ladbar", systemImage: "photo.badge.exclamationmark")
.foregroundStyle(.white)
default:
ProgressView().tint(.white)
}
if !path.isEmpty {
CachedAsyncImage(path: path) { img in
img.resizable().scaledToFit()
} placeholder: {
ProgressView().tint(.white)
}
}
}