/**
* TabataFit Player Screen
* Full-screen workout player with timer overlay
* Wired to shared data + useTimer hook
*/
import React, { useRef, useEffect, useCallback, useState } from 'react'
import {
View,
Text,
StyleSheet,
Pressable,
Animated,
Dimensions,
StatusBar,
} 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 Ionicons from '@expo/vector-icons/Ionicons'
import { useTimer } from '@/src/shared/hooks/useTimer'
import { useHaptics } from '@/src/shared/hooks/useHaptics'
import { useAudio } from '@/src/shared/hooks/useAudio'
import { useActivityStore } from '@/src/shared/stores'
import { getWorkoutById, getTrainerById } from '@/src/shared/data'
import {
BRAND,
DARK,
TEXT,
GLASS,
SHADOW,
PHASE_COLORS,
GRADIENTS,
} from '@/src/shared/constants/colors'
import { TYPOGRAPHY } from '@/src/shared/constants/typography'
import { SPACING } from '@/src/shared/constants/spacing'
import { RADIUS } from '@/src/shared/constants/borderRadius'
import { DURATION, EASE, SPRING } from '@/src/shared/constants/animations'
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window')
// ═══════════════════════════════════════════════════════════════════════════
// COMPONENTS
// ═══════════════════════════════════════════════════════════════════════════
type TimerPhase = 'PREP' | 'WORK' | 'REST' | 'COMPLETE'
function TimerRing({
progress,
phase,
size = 280,
}: {
progress: number
phase: TimerPhase
size?: number
}) {
const strokeWidth = 12
const phaseColor = PHASE_COLORS[phase].fill
return (
)
}
function PhaseIndicator({ phase }: { phase: TimerPhase }) {
const phaseColor = PHASE_COLORS[phase].fill
const phaseLabels: Record = {
PREP: 'GET READY',
WORK: 'WORK',
REST: 'REST',
COMPLETE: 'COMPLETE',
}
return (
{phaseLabels[phase]}
)
}
function ExerciseDisplay({
exercise,
nextExercise,
}: {
exercise: string
nextExercise?: string
}) {
return (
Current
{exercise}
{nextExercise && (
Next:
{nextExercise}
)}
)
}
function RoundIndicator({ current, total }: { current: number; total: number }) {
return (
Round {current}/{total}
)
}
function ControlButton({
icon,
onPress,
size = 64,
variant = 'primary',
}: {
icon: keyof typeof Ionicons.glyphMap
onPress: () => void
size?: number
variant?: 'primary' | 'secondary' | 'danger'
}) {
const scaleAnim = useRef(new Animated.Value(1)).current
const handlePressIn = () => {
Animated.spring(scaleAnim, {
toValue: 0.9,
...SPRING.SNAPPY,
useNativeDriver: true,
}).start()
}
const handlePressOut = () => {
Animated.spring(scaleAnim, {
toValue: 1,
...SPRING.BOUNCY,
useNativeDriver: true,
}).start()
}
const backgroundColor =
variant === 'primary'
? BRAND.PRIMARY
: variant === 'danger'
? '#FF3B30'
: 'rgba(255, 255, 255, 0.1)'
return (
)
}
function BurnBar({
currentCalories,
avgCalories,
}: {
currentCalories: number
avgCalories: number
}) {
const percentage = Math.min((currentCalories / avgCalories) * 100, 100)
return (
Burn Bar
{currentCalories} cal
Community avg: {avgCalories} cal
)
}
// ═══════════════════════════════════════════════════════════════════════════
// MAIN SCREEN
// ═══════════════════════════════════════════════════════════════════════════
export default function PlayerScreen() {
useKeepAwake()
const router = useRouter()
const { id } = useLocalSearchParams<{ id: string }>()
const insets = useSafeAreaInsets()
const haptics = useHaptics()
const addWorkoutResult = useActivityStore((s) => s.addWorkoutResult)
const workout = getWorkoutById(id ?? '1')
const trainer = workout ? getTrainerById(workout.trainerId) : null
const timer = useTimer(workout ?? null)
const audio = useAudio()
const [showControls, setShowControls] = useState(true)
// Animation refs
const timerScaleAnim = useRef(new Animated.Value(0.8)).current
const glowAnim = useRef(new Animated.Value(0)).current
const phaseColor = PHASE_COLORS[timer.phase].fill
// Format time
const formatTime = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return mins > 0 ? `${mins}:${secs.toString().padStart(2, '0')}` : `${secs}`
}
// Start timer
const startTimer = useCallback(() => {
timer.start()
haptics.buttonTap()
}, [timer, haptics])
// Pause/Resume
const togglePause = useCallback(() => {
if (timer.isPaused) {
timer.resume()
} else {
timer.pause()
}
haptics.selection()
}, [timer, haptics])
// Stop workout
const stopWorkout = useCallback(() => {
haptics.phaseChange()
timer.stop()
router.back()
}, [router, timer, haptics])
// Complete workout - go to celebration screen
const completeWorkout = useCallback(() => {
haptics.workoutComplete()
if (workout) {
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])
// Skip
const handleSkip = useCallback(() => {
timer.skip()
haptics.selection()
}, [timer, haptics])
// Toggle controls visibility
const toggleControls = useCallback(() => {
setShowControls(s => !s)
}, [])
// Entrance animation
useEffect(() => {
Animated.parallel([
Animated.spring(timerScaleAnim, {
toValue: 1,
friction: 6,
tension: 100,
useNativeDriver: true,
}),
Animated.loop(
Animated.sequence([
Animated.timing(glowAnim, {
toValue: 1,
duration: DURATION.BREATH,
easing: EASE.EASE_IN_OUT,
useNativeDriver: false,
}),
Animated.timing(glowAnim, {
toValue: 0,
duration: DURATION.BREATH,
easing: EASE.EASE_IN_OUT,
useNativeDriver: false,
}),
])
),
]).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 for last 3 seconds
useEffect(() => {
if (timer.isRunning && timer.timeRemaining <= 3 && timer.timeRemaining > 0) {
audio.countdownBeep()
}
}, [timer.timeRemaining])
const glowOpacity = glowAnim.interpolate({
inputRange: [0, 1],
outputRange: [0.3, 0.6],
})
return (
{/* Background gradient */}
{/* Phase glow */}
{/* Main content */}
{/* Header */}
{showControls && (
{workout?.title ?? 'Workout'}
with {trainer?.name ?? 'Coach'}
)}
{/* Timer */}
{formatTime(timer.timeRemaining)}
{/* Exercise */}
{!timer.isComplete && (
)}
{/* Complete state */}
{timer.isComplete && (
Workout Complete!
Great job!
{timer.totalRounds}
Rounds
{timer.calories}
Calories
{workout?.duration ?? 4}
Minutes
)}
{/* Controls */}
{showControls && !timer.isComplete && (
{!timer.isRunning ? (
) : (
)}
)}
{/* Complete button */}
{timer.isComplete && (
Done
)}
{/* Burn Bar */}
{showControls && timer.isRunning && !timer.isComplete && (
)}
)
}
// ═══════════════════════════════════════════════════════════════════════════
// STYLES
// ═══════════════════════════════════════════════════════════════════════════
const timerStyles = StyleSheet.create({
timerRingContainer: {
alignItems: 'center',
justifyContent: 'center',
},
timerRingBg: {
borderColor: 'rgba(255, 255, 255, 0.1)',
position: 'absolute',
},
timerRingContent: {
position: 'absolute',
},
timerProgressRing: {
position: 'absolute',
},
timerTextContainer: {
position: 'absolute',
alignItems: 'center',
},
phaseIndicator: {
paddingHorizontal: SPACING[4],
paddingVertical: SPACING[1],
borderRadius: RADIUS.FULL,
marginBottom: SPACING[2],
},
phaseText: {
...TYPOGRAPHY.CALLOUT,
fontWeight: '700',
letterSpacing: 1,
},
timerTime: {
...TYPOGRAPHY.TIMER_NUMBER,
color: TEXT.PRIMARY,
},
roundIndicator: {
marginTop: SPACING[2],
},
roundText: {
...TYPOGRAPHY.BODY,
color: TEXT.TERTIARY,
},
roundCurrent: {
color: TEXT.PRIMARY,
fontWeight: '700',
},
// Exercise
exerciseDisplay: {
alignItems: 'center',
marginTop: SPACING[6],
paddingHorizontal: SPACING[6],
},
currentExerciseLabel: {
...TYPOGRAPHY.CAPTION_1,
color: TEXT.TERTIARY,
textTransform: 'uppercase',
letterSpacing: 1,
},
currentExercise: {
...TYPOGRAPHY.TITLE_1,
color: TEXT.PRIMARY,
textAlign: 'center',
marginTop: SPACING[1],
},
nextExerciseContainer: {
flexDirection: 'row',
marginTop: SPACING[2],
},
nextExerciseLabel: {
...TYPOGRAPHY.BODY,
color: TEXT.TERTIARY,
},
nextExercise: {
...TYPOGRAPHY.BODY,
color: BRAND.PRIMARY,
},
// Controls
controlButton: {
alignItems: 'center',
justifyContent: 'center',
},
controlButtonBg: {
position: 'absolute',
width: '100%',
height: '100%',
borderRadius: 100,
},
// Burn Bar
burnBar: {},
burnBarHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: SPACING[2],
},
burnBarLabel: {
...TYPOGRAPHY.CAPTION_1,
color: TEXT.TERTIARY,
},
burnBarValue: {
...TYPOGRAPHY.CALLOUT,
color: BRAND.PRIMARY,
fontWeight: '600',
},
burnBarTrack: {
height: 6,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
borderRadius: 3,
overflow: 'hidden',
},
burnBarFill: {
height: '100%',
backgroundColor: BRAND.PRIMARY,
borderRadius: 3,
},
burnBarAvg: {
position: 'absolute',
top: -2,
width: 2,
height: 10,
backgroundColor: TEXT.TERTIARY,
},
burnBarAvgLabel: {
...TYPOGRAPHY.CAPTION_2,
color: TEXT.TERTIARY,
marginTop: SPACING[1],
textAlign: 'right',
},
})
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: DARK.BASE,
},
phaseGlow: {
position: 'absolute',
top: -100,
left: -100,
right: -100,
bottom: -100,
borderRadius: 500,
},
content: {
flex: 1,
},
// Header
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: SPACING[4],
},
closeButton: {
width: 44,
height: 44,
borderRadius: 22,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
headerCenter: {
alignItems: 'center',
},
workoutTitle: {
...TYPOGRAPHY.HEADLINE,
color: TEXT.PRIMARY,
},
workoutTrainer: {
...TYPOGRAPHY.CAPTION_1,
color: TEXT.TERTIARY,
},
// Timer
timerContainer: {
alignItems: 'center',
justifyContent: 'center',
marginTop: SPACING[8],
},
// Controls
controls: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
alignItems: 'center',
},
controlsRow: {
flexDirection: 'row',
alignItems: 'center',
gap: SPACING[6],
},
// Burn Bar
burnBarContainer: {
position: 'absolute',
left: SPACING[4],
right: SPACING[4],
height: 72,
borderRadius: RADIUS.LG,
overflow: 'hidden',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
padding: SPACING[3],
},
// Complete
completeContainer: {
alignItems: 'center',
marginTop: SPACING[8],
},
completeTitle: {
...TYPOGRAPHY.LARGE_TITLE,
color: TEXT.PRIMARY,
},
completeSubtitle: {
...TYPOGRAPHY.TITLE_3,
color: BRAND.PRIMARY,
marginTop: SPACING[1],
},
completeStats: {
flexDirection: 'row',
marginTop: SPACING[6],
gap: SPACING[8],
},
completeStat: {
alignItems: 'center',
},
completeStatValue: {
...TYPOGRAPHY.TITLE_1,
color: TEXT.PRIMARY,
},
completeStatLabel: {
...TYPOGRAPHY.CAPTION_1,
color: TEXT.TERTIARY,
marginTop: SPACING[1],
},
doneButton: {
width: 200,
height: 56,
borderRadius: RADIUS.GLASS_BUTTON,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
...SHADOW.BRAND_GLOW,
},
doneButtonText: {
...TYPOGRAPHY.BUTTON_MEDIUM,
color: TEXT.PRIMARY,
letterSpacing: 1,
},
})