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

@@ -20,12 +20,23 @@
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>tabatago</string>
</array>
</dict>
</array>
<key>NSHealthShareUsageDescription</key>
<string>TabataGo reads your health data to show fitness stats and personalize your workouts.</string>
<key>NSHealthUpdateUsageDescription</key>
<string>TabataGo saves your Tabata workouts to Apple Health to track calories, heart rate, and contribute to your Activity Rings.</string>
<key>NSMotionUsageDescription</key>
<string>TabataGo uses motion data to improve calorie estimates during workouts.</string>
<key>NSSupportsLiveActivities</key>
<true/>
<key>POSTHOG_API_KEY</key>
<string>$(POSTHOG_API_KEY)</string>
<key>REVENUECAT_API_KEY</key>

View File

@@ -9,9 +9,6 @@
},
"%@ / year — save 40%%" : {
},
"%@ bpm" : {
},
"%@ kcal" : {
@@ -2202,6 +2199,9 @@
}
}
}
},
"Next" : {
},
"No workouts yet" : {
"extractionState" : "manual",
@@ -5725,6 +5725,9 @@
}
}
}
},
"Skip" : {
},
"Subscription purchases are handled by Apple's App Store and RevenueCat. We do not store your payment information." : {
@@ -6284,25 +6287,25 @@
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ganzkörpertraining von Kopf bis Fuß"
"value" : "Zielt auf alle großen Muskelgruppen — das ultimative Ganzkörper-Tabata-Workout"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Total body burn, head to toe"
"value" : "Targets every major muscle group — the ultimate full-body Tabata burn"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Quema total del cuerpo, de pies a cabeza"
"value" : "Dirige a todos los grupos musculares principales — la quema Tabata de cuerpo completo definitiva"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Brûlure totale du corps, de la tête aux pieds"
"value" : "Cible tous les groupes musculaires — le brûleur Tabata corps entier ultime"
}
}
}
@@ -6342,25 +6345,25 @@
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Beine, Gesäß und Körpermitte"
"value" : "Zielt auf Quads, Gesäß, Beinbeuger und Waden mit explosiven Intervallen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Legs, glutes & core stability"
"value" : "Targets quads, glutes, hamstrings & calves with explosive intervals"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Piernas, glúteos y estabilidad del core"
"value" : "Dirige a cuádriceps, glúteos, isquiotibiales y pantorrillas con intervalos explosivos"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Jambes, fessiers et gainage"
"value" : "Cible quadriceps, fessiers, ischio-jambiers et mollets avec des intervals explosifs"
}
}
}
@@ -6400,25 +6403,25 @@
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Arme, Brust, Schultern und Rücken"
"value" : "Zielt auf Bizeps, Schultern, Brust und Rücken mit High-Intensity-Intervallen"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Arms, chest, shoulders & back"
"value" : "Targets biceps, shoulders, chest & back with high-intensity intervals"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Brazos, pecho, hombros y espalda"
"value" : "Dirige a bíceps, hombros, pecho y espalda con intervalos de alta intensidad"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Bras, poitrine, épaules et dos"
"value" : "Cible biceps, épaules, poitrine et dos avec des intervals haute intensité"
}
}
}