Phase 3.6: B+C+D komplett + HealthKit Sync

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.
This commit is contained in:
rene 2026-05-30 11:19:53 +02:00
parent 30e0fbe7ec
commit c01e3d6be7
26 changed files with 978 additions and 28 deletions

View file

@ -11,13 +11,17 @@ final class LocationTracker: NSObject, CLLocationManagerDelegate {
var points: [GPSPoint] = []
var isTracking: Bool = false
var isPaused: Bool = false
var isAutoPaused: Bool = false
var startedAt: Date?
var pausedAt: Date?
var accumulatedPausedSeconds: Int = 0
var accumulatedAutoPausedSeconds: Int = 0
var authorizationStatus: CLAuthorizationStatus
var permissionDenied: Bool = false
private var pendingStart: Bool = false
private var lastPointAt: Date?
private var autoPauseStartedAt: Date?
override init() {
self.authorizationStatus = manager.authorizationStatus
@ -93,6 +97,7 @@ final class LocationTracker: NSObject, CLLocationManagerDelegate {
if isPaused, let pausedAt {
accumulatedPausedSeconds += Int(Date.now.timeIntervalSince(pausedAt))
}
endAutoPauseIfNeeded()
isPaused = false
pausedAt = nil
manager.stopUpdatingLocation()
@ -102,17 +107,43 @@ final class LocationTracker: NSObject, CLLocationManagerDelegate {
// MARK: - Derived state
/// Active tracking time in seconds, with pauses already removed.
/// Active tracking time in seconds, with both manual and auto pauses removed.
var effectiveElapsedSeconds: Int {
guard let startedAt else { return 0 }
let total = Int(Date.now.timeIntervalSince(startedAt))
var paused = accumulatedPausedSeconds
var paused = accumulatedPausedSeconds + accumulatedAutoPausedSeconds
if isPaused, let pausedAt {
paused += Int(Date.now.timeIntervalSince(pausedAt))
}
if isAutoPaused, let autoPauseStartedAt {
paused += Int(Date.now.timeIntervalSince(autoPauseStartedAt))
}
return max(0, total - paused)
}
/// Called from a timer in TrackingView. If no new GPS point arrived in the
/// last 2 minutes, auto-pause the duration counter without stopping GPS
/// so we can detect the resume from the next location update.
func checkAutoPause() {
guard isTracking, !isPaused, !isAutoPaused else { return }
let enabled = UserDefaults.standard.object(forKey: "autoPauseEnabled") as? Bool ?? true
guard enabled else { return }
guard let last = lastPointAt else { return }
if Date.now.timeIntervalSince(last) > 120 {
isAutoPaused = true
autoPauseStartedAt = .now
}
}
private func endAutoPauseIfNeeded() {
guard isAutoPaused else { return }
if let started = autoPauseStartedAt {
accumulatedAutoPausedSeconds += Int(Date.now.timeIntervalSince(started))
}
isAutoPaused = false
autoPauseStartedAt = nil
}
var totalDistanceMeters: Double {
guard points.count >= 2 else { return 0 }
var total: Double = 0
@ -157,7 +188,13 @@ final class LocationTracker: NSObject, CLLocationManagerDelegate {
guard !newPoints.isEmpty else { return }
Task { @MainActor in
guard self.isTracking, !self.isPaused else { return }
// If we were auto-paused, the very arrival of a new GPS update
// means movement resumed.
self.endAutoPauseIfNeeded()
self.points.append(contentsOf: newPoints)
self.lastPointAt = .now
}
}