feat: redesign player with Dynamic Island, compact timer, and fix Live Activity timer drift
Some checks failed
CI / TypeScript (pull_request) Failing after 5s
CI / ESLint (pull_request) Failing after 3s
CI / Tests (pull_request) Failing after 5s
CI / Build Check (pull_request) Has been skipped
CI / Admin Web Tests (pull_request) Successful in 2m5s
CI / Deploy Edge Functions (pull_request) Has been skipped
Some checks failed
CI / TypeScript (pull_request) Failing after 5s
CI / ESLint (pull_request) Failing after 3s
CI / Tests (pull_request) Failing after 5s
CI / Build Check (pull_request) Has been skipped
CI / Admin Web Tests (pull_request) Successful in 2m5s
CI / Deploy Edge Functions (pull_request) Has been skipped
## What changed ### Player Redesign (video-first layout) - New compact timer ring (110pt) with phase label, replaces 240pt ring - Auto-hide top bar with block progress dots (3s auto-dismiss) - Expandable now-playing music pill with skip control - Bottom control bar with heart rate, play/pause, and skip - Exercise caption with 'Next' preview during rest phases - Compact round counter (capsule dots) ### Dynamic Island & Live Activities - WorkoutLiveActivity widget: expanded, compact, and minimal views - Phase-colored timers with Text(timerInterval:) countdown - Shows exercise name, round progress, heart rate, music track - MusicLiveActivity: standalone music now-playing widget - LiveActivityMusicBars animated component - Deep link from Dynamic Island back to app ### Timer Drift Fix (critical) - Store a stable phaseEndDate once per phase instead of recalculating Date() + timeRemaining on every update - Prevents dynamic island countdown from rubber-banding due to 5-second periodic update recalculation drift - Reset phaseEndDate on phase change and resume from pause - Guard Live Activity updates behind vm.isRunning to prevent premature creation when music track loads before workout start - Fixes timer showing 0 in Dynamic Island when expanding from home screen ### New PlayerViewModel timer engine - Full phase support: prep, warmup, work, rest, interBlockRest, cooldown, complete - 1-second countdown with audio cues at 3-2-1 - Phase transitions with spring animation and haptics - HealthKit live session integration - Workout session recording with completion ### Music Service - New MusicPlayerViewModel with vibe-based playlist loading - Track info exposed for Dynamic Island display - Skip track support from Dynamic Island notification action - Automatic play/pause based on phase and running state ### Additional - ZoneHighlightIcon component for HomeTab zone cards - Updated watchOS localizations with complication strings - Info.plist updated for widget extension
This commit is contained in:
@@ -56,6 +56,15 @@ final class PlayerViewModel: ObservableObject {
|
||||
return program.blocks[currentBlockIndex]
|
||||
}
|
||||
|
||||
var blockProgress: Double {
|
||||
guard !program.blocks.isEmpty else { return 1 }
|
||||
return Double(currentBlockIndex + 1) / Double(program.blocks.count)
|
||||
}
|
||||
|
||||
var isRestPhase: Bool {
|
||||
phase == .rest || phase == .interBlockRest
|
||||
}
|
||||
|
||||
private let audio = AudioService.shared
|
||||
private let haptics = UIImpactFeedbackGenerator(style: .rigid)
|
||||
private let softHaptics = UIImpactFeedbackGenerator(style: .soft)
|
||||
@@ -107,13 +116,13 @@ final class PlayerViewModel: ObservableObject {
|
||||
// Start HealthKit live session
|
||||
Task {
|
||||
try? await HealthKitService.shared.requestAuthorization()
|
||||
try? await liveSession.start(startDate: startedAt!)
|
||||
liveSession.onHeartRateUpdate = { [weak self] hr in
|
||||
Task { @MainActor in self?.heartRate = hr }
|
||||
}
|
||||
liveSession.onCaloriesUpdate = { [weak self] cal in
|
||||
Task { @MainActor in self?.liveCalories = cal }
|
||||
}
|
||||
try? await liveSession.start(startDate: startedAt!)
|
||||
}
|
||||
|
||||
AnalyticsService.shared.workoutStarted(
|
||||
|
||||
Reference in New Issue
Block a user