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:
parent
30e0fbe7ec
commit
c01e3d6be7
26 changed files with 978 additions and 28 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue