feat: redesign Dynamic Island with phase-driven UI and animations

This commit is contained in:
Millian Lamiaux
2026-05-21 10:21:22 +02:00
parent 67e2bdc8c3
commit c152c22ffb
12 changed files with 472 additions and 76 deletions

View File

@@ -57,6 +57,8 @@ final class MusicPlayerViewModel: ObservableObject {
func play() {
guard audio.isMusicEnabled, player != nil else { return }
// Reactivate the audio session in case the system deactivated it while backgrounded
try? AVAudioSession.sharedInstance().setActive(true)
player?.volume = audio.musicVolume
player?.play()
}

View File

@@ -58,6 +58,8 @@ final class PlayerViewModel: ObservableObject {
private var warmupIndex: Int = 0
// Cooldown phase index
private var cooldownIndex: Int = 0
// Throttle rapid toggle from widget
private var lastToggleTimestamp: Date = .distantPast
private var currentBlock: TabataBlock? {
guard currentBlockIndex < program.blocks.count else { return nil }
@@ -91,6 +93,14 @@ final class PlayerViewModel: ObservableObject {
// Controls
func togglePlayPause() {
let now = Date()
guard now.timeIntervalSince(lastToggleTimestamp) > 0.6 else {
print("[PlayerVM] TogglePlayPause throttled (last tap was < 0.6s ago)")
return
}
lastToggleTimestamp = now
print("[PlayerVM] TogglePlayPause — isPaused=\(isPaused), isRunning=\(isRunning)")
if !isRunning {
startWorkout()
} else if isPaused {
@@ -384,7 +394,11 @@ final class PlayerViewModel: ObservableObject {
trackTitle: currentTrackTitle,
trackArtist: currentTrackArtist,
isPlaying: isPlayingMusic,
isPaused: isPaused
isPaused: isPaused,
blockIndex: currentBlockIndex + 1,
blockCount: program.blocks.count,
exerciseShortName: String(currentExercise?.nameEn.prefix(8) ?? ""),
phaseElapsedSeconds: max(0, Double(totalPhaseTime) - Double(timeRemaining))
)
if let existing = workoutActivity, existing.activityState != .active {
@@ -452,7 +466,11 @@ final class PlayerViewModel: ObservableObject {
trackTitle: safeActivity.content.state.trackTitle,
trackArtist: safeActivity.content.state.trackArtist,
isPlaying: false,
isPaused: false
isPaused: false,
blockIndex: safeActivity.content.state.blockIndex,
blockCount: safeActivity.content.state.blockCount,
exerciseShortName: safeActivity.content.state.exerciseShortName,
phaseElapsedSeconds: safeActivity.content.state.phaseElapsedSeconds
)
await safeActivity.end(ActivityContent(state: finalState, staleDate: nil), dismissalPolicy: .immediate)
}