/** * TabataFit Player Screen * Thin orchestrator — all UI extracted to src/features/player/ * FORCE DARK — always uses darkColors regardless of system theme */ import React, { useRef, useEffect, useCallback, useState } from 'react' import { View, Text, StyleSheet, Pressable, Animated, StatusBar, useWindowDimensions, } from 'react-native' import { useRouter, useLocalSearchParams } from 'expo-router' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { LinearGradient } from 'expo-linear-gradient' import { BlurView } from 'expo-blur' import { useKeepAwake } from 'expo-keep-awake' import { Icon } from '@/src/shared/components/Icon' import { useTranslation } from 'react-i18next' import { useTimer } from '@/src/shared/hooks/useTimer' import { useHaptics } from '@/src/shared/hooks/useHaptics' import { useAudio } from '@/src/shared/hooks/useAudio' import { useMusicPlayer } from '@/src/shared/hooks/useMusicPlayer' import { useActivityStore } from '@/src/shared/stores' import { getWorkoutById, getTrainerById, getWorkoutAccentColor } from '@/src/shared/data' import { useTranslatedWorkout } from '@/src/shared/data/useTranslatedData' import { useWatchSync } from '@/src/features/watch' import { track } from '@/src/shared/services/analytics' import { VideoPlayer } from '@/src/shared/components/VideoPlayer' import { PHASE_COLORS, GRADIENTS, darkColors } from '@/src/shared/theme' import { TYPOGRAPHY } from '@/src/shared/constants/typography' import { SPACING } from '@/src/shared/constants/spacing' import { RADIUS } from '@/src/shared/constants/borderRadius' import { TimerRing, PhaseIndicator, ExerciseDisplay, RoundIndicator, PlayerControls, BurnBar, StatsOverlay, CoachEncouragement, NowPlaying, } from '@/src/features/player' // ─── Helpers ───────────────────────────────────────────────────────────────── function formatTime(seconds: number) { const mins = Math.floor(seconds / 60) const secs = seconds % 60 return `${mins}:${secs.toString().padStart(2, '0')}` } // ─── Main Screen ───────────────────────────────────────────────────────────── export default function PlayerScreen() { useKeepAwake() const router = useRouter() const { id } = useLocalSearchParams<{ id: string }>() const insets = useSafeAreaInsets() const haptics = useHaptics() const { t } = useTranslation() const { width: SCREEN_WIDTH } = useWindowDimensions() const addWorkoutResult = useActivityStore((s) => s.addWorkoutResult) const colors = darkColors const rawWorkout = getWorkoutById(id ?? '1') const workout = useTranslatedWorkout(rawWorkout) const trainer = rawWorkout ? getTrainerById(rawWorkout.trainerId) : undefined const trainerColor = getWorkoutAccentColor(id ?? '1') const timer = useTimer(rawWorkout ?? null) const audio = useAudio() // Music player — synced with workout timer const music = useMusicPlayer({ vibe: workout?.musicVibe ?? 'electronic', isPlaying: timer.isRunning && !timer.isPaused, }) const [showControls, setShowControls] = useState(true) const [heartRate, setHeartRate] = useState(null) // Watch sync integration const { isAvailable: isWatchAvailable, sendWorkoutState } = useWatchSync({ onPlay: () => { timer.resume() track('watch_control_play', { workout_id: workout?.id ?? id }) }, onPause: () => { timer.pause() track('watch_control_pause', { workout_id: workout?.id ?? id }) }, onSkip: () => { timer.skip() haptics.selection() track('watch_control_skip', { workout_id: workout?.id ?? id }) }, onStop: () => { haptics.phaseChange() timer.stop() router.back() track('watch_control_stop', { workout_id: workout?.id ?? id }) }, onHeartRateUpdate: (hr: number) => setHeartRate(hr), }) // Animation refs const timerScaleAnim = useRef(new Animated.Value(0.8)).current const phaseColor = PHASE_COLORS[timer.phase].fill // ─── Actions ───────────────────────────────────────────────────────────── const startTimer = useCallback(() => { timer.start() haptics.buttonTap() if (workout) { track('workout_started', { workout_id: workout.id, workout_title: workout.title, duration: workout.duration, level: workout.level, }) } }, [timer, haptics, workout]) const togglePause = useCallback(() => { const workoutId = workout?.id ?? id ?? '' if (timer.isPaused) { timer.resume() track('workout_resumed', { workout_id: workoutId }) } else { timer.pause() track('workout_paused', { workout_id: workoutId }) } haptics.selection() }, [timer, haptics, workout, id]) const stopWorkout = useCallback(() => { haptics.phaseChange() timer.stop() router.back() }, [router, timer, haptics]) const completeWorkout = useCallback(() => { haptics.workoutComplete() if (workout) { track('workout_completed', { workout_id: workout.id, workout_title: workout.title, calories: timer.calories, duration: workout.duration, rounds: workout.rounds, }) addWorkoutResult({ id: Date.now().toString(), workoutId: workout.id, completedAt: Date.now(), calories: timer.calories, durationMinutes: workout.duration, rounds: workout.rounds, completionRate: 1, }) } router.replace(`/complete/${workout?.id ?? '1'}`) }, [router, workout, timer.calories, haptics, addWorkoutResult]) const handleSkip = useCallback(() => { timer.skip() haptics.selection() }, [timer, haptics]) const toggleControls = useCallback(() => { setShowControls((s) => !s) }, []) // ─── Animations & side-effects ─────────────────────────────────────────── // Entrance animation useEffect(() => { Animated.spring(timerScaleAnim, { toValue: 1, friction: 6, tension: 100, useNativeDriver: true, }).start() }, []) // Phase change animation + audio useEffect(() => { timerScaleAnim.setValue(0.9) Animated.spring(timerScaleAnim, { toValue: 1, friction: 4, tension: 150, useNativeDriver: true, }).start() haptics.phaseChange() if (timer.phase === 'COMPLETE') { audio.workoutComplete() } else if (timer.isRunning) { audio.phaseStart() } }, [timer.phase]) // Countdown beep + haptic for last 3 seconds useEffect(() => { if (timer.isRunning && timer.timeRemaining <= 3 && timer.timeRemaining > 0) { audio.countdownBeep() haptics.countdownTick() } }, [timer.timeRemaining]) // Sync workout state with Apple Watch useEffect(() => { if (!isWatchAvailable || !timer.isRunning) return sendWorkoutState({ phase: timer.phase, timeRemaining: timer.timeRemaining, currentRound: timer.currentRound, totalRounds: timer.totalRounds, currentExercise: timer.currentExercise, nextExercise: timer.nextExercise, calories: timer.calories, isPaused: timer.isPaused, isPlaying: timer.isRunning && !timer.isPaused, }) }, [ timer.phase, timer.timeRemaining, timer.currentRound, timer.totalRounds, timer.currentExercise, timer.nextExercise, timer.calories, timer.isPaused, timer.isRunning, isWatchAvailable, ]) // ─── Render ────────────────────────────────────────────────────────────── return (