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

## 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:
Millian Lamiaux
2026-04-25 23:51:46 +02:00
parent 7f5ea9c6e9
commit b0d364eca2
17 changed files with 1797 additions and 377 deletions

View File

@@ -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(