feat: redesign player with Dynamic Island, compact timer, and fix Live Activity timer drift #2

Merged
millianlmx merged 18 commits from revamp-timer-video-layout into main 2026-05-23 12:24:34 +02:00

18 Commits

Author SHA1 Message Date
Millian Lamiaux
38576fd528 ci: replace dead Expo CI with linux-only monorepo pipeline
Some checks failed
CI / Admin Web CI (pull_request) Failing after 34s
CI / YouTube Worker (pull_request) Failing after 5s
CI / Deploy (pull_request) Has been skipped
CI / Detect Changes (pull_request) Successful in 6s
Remove: root Expo typecheck/lint/test/build-check (project deleted Apr 19)
Remove: swift-build-test + app-store.yml (no macOS runner on Gitea)
Add: path-filtered conditional jobs with dorny/paths-filter@v3
Add: youtube-worker deploy to deploy-functions job
Fix: deploy-functions always() to handle skipped upstream jobs
Fix: SPM cache key includes Package.resolved (before removal)
Fix: remove continue-on-error from vitest/playwright steps
2026-05-23 12:09:28 +02:00
Millian Lamiaux
df9fd48964 chore: update tabatago-swift submodule (Live Activity fix)
Some checks failed
CI / TypeScript (pull_request) Failing after 4s
CI / ESLint (pull_request) Failing after 4s
CI / Tests (pull_request) Failing after 6s
CI / Build Check (pull_request) Has been skipped
CI / Admin Web Tests (pull_request) Successful in 2m7s
CI / Deploy Edge Functions (pull_request) Has been skipped
Includes fix for stale Live Activity persisting after workout
cancel/background. See submodule commit e42c121 for details.
2026-05-23 00:41:41 +02:00
Millian Lamiaux
e42c1217db fix: Live Activity persists after workout cancel/background
Root cause: observeActivityState() prematurely set workoutActivity=nil
when the activity went .stale (e.g. app backgrounded >2 minutes). This
prevented endActivity() from calling .end() on the stale activity,
leaving it visible on the Lock Screen and Dynamic Island indefinitely.

Fixes (all in PlayerViewModel.swift):

1. observeActivityState(): Split the monolithic stale/ended/dismissed
   handler. .stale now only stops the sync timer but keeps the
   workoutActivity reference so endActivity() can still call .end()
   to properly dismiss the stale Live Activity.

2. syncActivity() nil guard: Changed from != .active to explicit
   == .ended || == .dismissed so stale activities are not prematurely
   discarded when tick() re-enters syncActivity().

3. endActivity(): Added stopActivitySyncTimer() defensive call at top
   to prevent orphaned timer from racing in and recreating the activity
   during .end(). Also relaxed the guard from == .active to
   != .ended && != .dismissed so stale activities can be ended.

4. abandonWorkout(): Explicitly set isRunning=false + isPaused=false
   before cleanup to prevent accidental Live Activity recreation.
2026-05-23 00:40:41 +02:00
Millian Lamiaux
cd6fea9b53 ci: add App Store submission pipeline via GitHub Actions
Some checks failed
CI / TypeScript (pull_request) Failing after 4s
CI / ESLint (pull_request) Failing after 4s
CI / Tests (pull_request) Failing after 12s
CI / Build Check (pull_request) Has been skipped
CI / Admin Web Tests (pull_request) Successful in 2m7s
CI / Deploy Edge Functions (pull_request) Has been skipped
2026-05-21 10:47:48 +02:00
Millian Lamiaux
d31b769ab8 chore: update docs and remove stale skill files 2026-05-21 10:21:57 +02:00
Millian Lamiaux
c152c22ffb feat: redesign Dynamic Island with phase-driven UI and animations 2026-05-21 10:21:22 +02:00
Millian Lamiaux
67e2bdc8c3 Redesign workout live activity with circular timer ring, phase icons, and smoother updates
Some checks failed
CI / TypeScript (pull_request) Failing after 19s
CI / ESLint (pull_request) Failing after 4s
CI / Tests (pull_request) Failing after 7s
CI / Build Check (pull_request) Has been skipped
CI / Admin Web Tests (pull_request) Successful in 2m9s
CI / Deploy Edge Functions (pull_request) Has been skipped
- Add CountdownRing with real-time arc progress on lock screen
- Replace generic dots with phase-specific SF Symbols (flame, snowflake, etc.)
- Remove horizontal progress bar in favor of round counter text
- Increase Dynamic Island expanded font sizes for better visibility
- Increase live activity sync frequency from 5s to 1s for smoother arc updates
- Add pause/resume button via TogglePauseIntent AppIntent
- Remove AlertConfiguration to silence notification sounds on updates
2026-05-17 00:43:01 +02:00
Millian Lamiaux
dc3ff15e81 feat: production-grade Live Activity with type-safe phases, decomposed views, previews, and alert transitions
- Replace raw string phase model with WorkoutPhase enum (Codable, Sendable, CaseIterable)
  with built-in .capitalized display name and SwiftUI .color per phase
- Decompose WorkoutLiveActivity into reusable view structs: PhasePill, CountdownText,
  WorkoutProgressBar, MusicInfoRow, HeartRateBadge, PhaseIndicatorDot, WorkoutLockScreenView,
  WorkoutSmallView — following CraftingSwift iOS 26 architecture patterns
- Add AlertConfiguration on work/rest/complete phase transitions so Dynamic Island
  expands and lights up at key moments
- Add 13 #Preview blocks across both widgets covering all presentation types:
  lock screen, expanded, compact, minimal — for instant Xcode Canvas feedback
- Add stale state handling (context.isStale shows 'Last updated' indicator)
- MusicLiveActivity: 5 new #Preview blocks for playing/paused/expanded/compact/minimal
2026-05-16 15:28:45 +02:00
Millian Lamiaux
95f34e6471 feat: Dynamic Island pause state, Apple-aligned spacing, and UI polish
- Add isPaused to WorkoutActivityAttributes.ContentState
- Show PAUSED badge, freeze timer to static text, dim content when paused
- Prevent stale spinner on pause by extending staleDate to 1 hour
- Add 6s timer warning color, progress bar, compact heavy timer
- Pulsing compact indicator during WORK phase
- Lock Screen margins aligned to Apple's 14pt HIG spec
2026-05-15 23:52:01 +02:00
Millian Lamiaux
057fbb3c9a fix: add 6s timeout to MusicService Supabase fetch for offline fallback
When airplane mode is active, the Supabase client hung indefinitely
waiting for a network response, blocking the mock track fallback.
Now races the query against a 6-second Task.sleep so mock tracks
load immediately after timeout.
2026-05-15 23:51:51 +02:00
Millian Lamiaux
918e663dbf chore: update Xcode project for widget target and watch team
- Add widget target PBXBuildFile, PBXFileReference, PBXGroup, PBXNativeTarget entries
- Reorder PBXCopyFilesBuildPhase and XCBuildConfiguration sections
- Add DEVELOPMENT_TEAM to watch complication and watch app configs
2026-05-15 22:41:35 +02:00
Millian Lamiaux
fe005ee7f3 feat: Live Activity accessibility and supplemental families (small/medium)
- Add @Environment activityFamily, isActivityFullscreen, isLuminanceReduced
- Split into lockScreenView() and smallLockScreenView() variants
- Add supplementalActivityFamilies([.small, .medium]) support
- Add keylineTint and contentMargins to Dynamic Island
- Add accessibility labels throughout (VoiceOver support)
- Hide music bar animation when isLuminanceReduced
2026-05-15 22:41:20 +02:00
Millian Lamiaux
71de3c0aa7 fix: Live Activity concurrency and state observation
- Add Sendable conformance to MusicActivityAttributes.ContentState
- Remove @preconcurrency on ActivityKit import
- Use nonisolated(unsafe) guards for Activity refs in task closures
- Add observeActivityState() to handle stale/ended/dismissed activity states
- Set staleDate (120s) instead of nil for push notification support
2026-05-15 22:41:04 +02:00
Millian Lamiaux
03f660958f add agent skills and opencode config 2026-05-10 20:09:13 +01:00
Millian Lamiaux
349a96379e add mock programs fallback for offline development 2026-05-10 19:20:30 +01:00
Millian Lamiaux
d43142641f refactor: clean up Dynamic Island expanded layout with phase badge, symmetric edges, and music+heart rate in bottom region 2026-05-03 20:59:03 +02:00
Millian Lamiaux
c715c797f9 fix: move Live Activity ownership to ViewModel, fix timer-at-0 and background freeze
**Architecture (PlayerViewModel):**
- Move ActivityKit lifecycle from SwiftUI View to ViewModel (MVVM correction)
- call syncActivity() at END of enterPhase() — after all state is set,
  eliminating the race where phase was Published before timeRemaining
- Always recalculate phaseEndDate = Date() + timeRemaining (no stale cache)
- Dedicated Timer in ViewModel for periodic heart-rate/track sync (5s)
- Start/stop activity sync timer on play/pause/resume/abandon/finish
- stale activity reference discard + recreate-on-failure fallback
- Modern iOS 16.2+ API: ActivityContent, non-throwing update()

**PlayerView:**
- Remove all ActivityKit code (import, @State workoutActivity,
  phaseEndDate, dynamicIslandAvailable, 4 methods, .onReceive timer)
- Delegate to ViewModel: onChange(musicVM.currentTrack) sets vm.trackTitle/Artist
  and calls vm.syncActivity(); onDisappear calls await vm.endActivity()
- Music/audio onChange handlers no longer contain activity logic

**Info.plist:**
- Add UIBackgroundModes → audio so music continues and app stays alive
  in background, allowing Timer-based activity updates
- Widget Info.plist: add NSSupportsLiveActivitiesFrequentUpdates

**WorkoutActivityAttributes.ContentState:**
- Add Sendable conformance for Swift 6 strict concurrency

Fixes: timer stuck at 0 on first work phase, exercise name missing,
       music stopping in background, Dynamic Island freezing in background,
       widget drift due to cached phaseEndDate
2026-05-03 15:40:36 +02:00
Millian Lamiaux
b0d364eca2 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
2026-04-25 23:51:46 +02:00