feat: integrate theme and i18n across all screens
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,18 +2,21 @@
|
||||
* TabataFit Player Screen
|
||||
* Full-screen workout player with timer overlay
|
||||
* Wired to shared data + useTimer hook
|
||||
* FORCE DARK — always uses darkColors regardless of system theme
|
||||
*/
|
||||
|
||||
import React, { useRef, useEffect, useCallback, useState } from 'react'
|
||||
import React, { useRef, useEffect, useCallback, useState, useMemo } from 'react'
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Pressable,
|
||||
Animated,
|
||||
Easing,
|
||||
Dimensions,
|
||||
StatusBar,
|
||||
} from 'react-native'
|
||||
import Svg, { Circle } from 'react-native-svg'
|
||||
import { useRouter, useLocalSearchParams } from 'expo-router'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
@@ -21,25 +24,22 @@ import { BlurView } from 'expo-blur'
|
||||
import { useKeepAwake } from 'expo-keep-awake'
|
||||
import Ionicons from '@expo/vector-icons/Ionicons'
|
||||
|
||||
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 { useActivityStore } from '@/src/shared/stores'
|
||||
import { getWorkoutById, getTrainerById } from '@/src/shared/data'
|
||||
import { getWorkoutById } from '@/src/shared/data'
|
||||
import { useTranslatedWorkout } from '@/src/shared/data/useTranslatedData'
|
||||
|
||||
import {
|
||||
BRAND,
|
||||
DARK,
|
||||
TEXT,
|
||||
GLASS,
|
||||
SHADOW,
|
||||
PHASE_COLORS,
|
||||
GRADIENTS,
|
||||
} from '@/src/shared/constants/colors'
|
||||
import { track } from '@/src/shared/services/analytics'
|
||||
import { BRAND, PHASE_COLORS, GRADIENTS, darkColors } from '@/src/shared/theme'
|
||||
import type { ThemeColors } from '@/src/shared/theme/types'
|
||||
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'
|
||||
import { SPRING } from '@/src/shared/constants/animations'
|
||||
|
||||
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window')
|
||||
|
||||
@@ -49,6 +49,8 @@ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window')
|
||||
|
||||
type TimerPhase = 'PREP' | 'WORK' | 'REST' | 'COMPLETE'
|
||||
|
||||
const AnimatedCircle = Animated.createAnimatedComponent(Circle)
|
||||
|
||||
function TimerRing({
|
||||
progress,
|
||||
phase,
|
||||
@@ -58,48 +60,78 @@ function TimerRing({
|
||||
phase: TimerPhase
|
||||
size?: number
|
||||
}) {
|
||||
const colors = darkColors
|
||||
const strokeWidth = 12
|
||||
const radius = (size - strokeWidth) / 2
|
||||
const circumference = 2 * Math.PI * radius
|
||||
const phaseColor = PHASE_COLORS[phase].fill
|
||||
|
||||
const animatedProgress = useRef(new Animated.Value(0)).current
|
||||
const prevProgress = useRef(0)
|
||||
|
||||
useEffect(() => {
|
||||
// If progress jumped backwards (new phase started), snap instantly
|
||||
if (progress < prevProgress.current - 0.05) {
|
||||
animatedProgress.setValue(progress)
|
||||
} else {
|
||||
Animated.timing(animatedProgress, {
|
||||
toValue: progress,
|
||||
duration: 1000,
|
||||
easing: Easing.linear,
|
||||
useNativeDriver: false,
|
||||
}).start()
|
||||
}
|
||||
prevProgress.current = progress
|
||||
}, [progress])
|
||||
|
||||
const strokeDashoffset = animatedProgress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [circumference, 0],
|
||||
})
|
||||
|
||||
const timerStyles = useMemo(() => createTimerStyles(colors), [colors])
|
||||
|
||||
return (
|
||||
<View style={[timerStyles.timerRingContainer, { width: size, height: size }]}>
|
||||
<View
|
||||
style={[
|
||||
timerStyles.timerRingBg,
|
||||
{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
borderWidth: strokeWidth,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<View style={timerStyles.timerRingContent}>
|
||||
<View
|
||||
style={[
|
||||
timerStyles.timerProgressRing,
|
||||
{
|
||||
width: size - strokeWidth * 2,
|
||||
height: size - strokeWidth * 2,
|
||||
borderRadius: (size - strokeWidth * 2) / 2,
|
||||
borderColor: phaseColor,
|
||||
borderTopWidth: strokeWidth,
|
||||
opacity: progress,
|
||||
},
|
||||
]}
|
||||
<Svg width={size} height={size}>
|
||||
{/* Background track */}
|
||||
<Circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
stroke={colors.border.glass}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
/>
|
||||
</View>
|
||||
{/* Progress arc */}
|
||||
<AnimatedCircle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={radius}
|
||||
stroke={phaseColor}
|
||||
strokeWidth={strokeWidth}
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
rotation="-90"
|
||||
origin={`${size / 2}, ${size / 2}`}
|
||||
/>
|
||||
</Svg>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function PhaseIndicator({ phase }: { phase: TimerPhase }) {
|
||||
const { t } = useTranslation()
|
||||
const colors = darkColors
|
||||
const timerStyles = useMemo(() => createTimerStyles(colors), [colors])
|
||||
const phaseColor = PHASE_COLORS[phase].fill
|
||||
const phaseLabels: Record<TimerPhase, string> = {
|
||||
PREP: 'GET READY',
|
||||
WORK: 'WORK',
|
||||
REST: 'REST',
|
||||
COMPLETE: 'COMPLETE',
|
||||
PREP: t('screens:player.phases.prep'),
|
||||
WORK: t('screens:player.phases.work'),
|
||||
REST: t('screens:player.phases.rest'),
|
||||
COMPLETE: t('screens:player.phases.complete'),
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -116,13 +148,16 @@ function ExerciseDisplay({
|
||||
exercise: string
|
||||
nextExercise?: string
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const colors = darkColors
|
||||
const timerStyles = useMemo(() => createTimerStyles(colors), [colors])
|
||||
return (
|
||||
<View style={timerStyles.exerciseDisplay}>
|
||||
<Text style={timerStyles.currentExerciseLabel}>Current</Text>
|
||||
<Text style={timerStyles.currentExerciseLabel}>{t('screens:player.current')}</Text>
|
||||
<Text style={timerStyles.currentExercise}>{exercise}</Text>
|
||||
{nextExercise && (
|
||||
<View style={timerStyles.nextExerciseContainer}>
|
||||
<Text style={timerStyles.nextExerciseLabel}>Next: </Text>
|
||||
<Text style={timerStyles.nextExerciseLabel}>{t('screens:player.next')}</Text>
|
||||
<Text style={timerStyles.nextExercise}>{nextExercise}</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -131,10 +166,13 @@ function ExerciseDisplay({
|
||||
}
|
||||
|
||||
function RoundIndicator({ current, total }: { current: number; total: number }) {
|
||||
const { t } = useTranslation()
|
||||
const colors = darkColors
|
||||
const timerStyles = useMemo(() => createTimerStyles(colors), [colors])
|
||||
return (
|
||||
<View style={timerStyles.roundIndicator}>
|
||||
<Text style={timerStyles.roundText}>
|
||||
Round <Text style={timerStyles.roundCurrent}>{current}</Text>/{total}
|
||||
{t('screens:player.round')} <Text style={timerStyles.roundCurrent}>{current}</Text>/{total}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
@@ -151,6 +189,8 @@ function ControlButton({
|
||||
size?: number
|
||||
variant?: 'primary' | 'secondary' | 'danger'
|
||||
}) {
|
||||
const colors = darkColors
|
||||
const timerStyles = useMemo(() => createTimerStyles(colors), [colors])
|
||||
const scaleAnim = useRef(new Animated.Value(1)).current
|
||||
|
||||
const handlePressIn = () => {
|
||||
@@ -174,7 +214,7 @@ function ControlButton({
|
||||
? BRAND.PRIMARY
|
||||
: variant === 'danger'
|
||||
? '#FF3B30'
|
||||
: 'rgba(255, 255, 255, 0.1)'
|
||||
: colors.border.glass
|
||||
|
||||
return (
|
||||
<Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
|
||||
@@ -185,7 +225,7 @@ function ControlButton({
|
||||
style={[timerStyles.controlButton, { width: size, height: size, borderRadius: size / 2 }]}
|
||||
>
|
||||
<View style={[timerStyles.controlButtonBg, { backgroundColor }]} />
|
||||
<Ionicons name={icon} size={size * 0.4} color={TEXT.PRIMARY} />
|
||||
<Ionicons name={icon} size={size * 0.4} color={colors.text.primary} />
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
)
|
||||
@@ -198,19 +238,22 @@ function BurnBar({
|
||||
currentCalories: number
|
||||
avgCalories: number
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const colors = darkColors
|
||||
const timerStyles = useMemo(() => createTimerStyles(colors), [colors])
|
||||
const percentage = Math.min((currentCalories / avgCalories) * 100, 100)
|
||||
|
||||
return (
|
||||
<View style={timerStyles.burnBar}>
|
||||
<View style={timerStyles.burnBarHeader}>
|
||||
<Text style={timerStyles.burnBarLabel}>Burn Bar</Text>
|
||||
<Text style={timerStyles.burnBarValue}>{currentCalories} cal</Text>
|
||||
<Text style={timerStyles.burnBarLabel}>{t('screens:player.burnBar')}</Text>
|
||||
<Text style={timerStyles.burnBarValue}>{t('units.calUnit', { count: currentCalories })}</Text>
|
||||
</View>
|
||||
<View style={timerStyles.burnBarTrack}>
|
||||
<View style={[timerStyles.burnBarFill, { width: `${percentage}%` }]} />
|
||||
<View style={[timerStyles.burnBarAvg, { left: '50%' }]} />
|
||||
</View>
|
||||
<Text style={timerStyles.burnBarAvgLabel}>Community avg: {avgCalories} cal</Text>
|
||||
<Text style={timerStyles.burnBarAvgLabel}>{t('screens:player.communityAvg', { calories: avgCalories })}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
@@ -225,44 +268,57 @@ export default function PlayerScreen() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>()
|
||||
const insets = useSafeAreaInsets()
|
||||
const haptics = useHaptics()
|
||||
const { t } = useTranslation()
|
||||
const addWorkoutResult = useActivityStore((s) => s.addWorkoutResult)
|
||||
|
||||
const workout = getWorkoutById(id ?? '1')
|
||||
const trainer = workout ? getTrainerById(workout.trainerId) : null
|
||||
const colors = darkColors
|
||||
const styles = useMemo(() => createStyles(colors), [colors])
|
||||
const timerStyles = useMemo(() => createTimerStyles(colors), [colors])
|
||||
|
||||
const timer = useTimer(workout ?? null)
|
||||
const rawWorkout = getWorkoutById(id ?? '1')
|
||||
const workout = useTranslatedWorkout(rawWorkout)
|
||||
const timer = useTimer(rawWorkout ?? 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}`
|
||||
return `${mins}:${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
// Start timer
|
||||
const startTimer = useCallback(() => {
|
||||
timer.start()
|
||||
haptics.buttonTap()
|
||||
}, [timer, haptics])
|
||||
if (workout) {
|
||||
track('workout_started', {
|
||||
workout_id: workout.id,
|
||||
workout_title: workout.title,
|
||||
duration: workout.duration,
|
||||
level: workout.level,
|
||||
})
|
||||
}
|
||||
}, [timer, haptics, workout])
|
||||
|
||||
// Pause/Resume
|
||||
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])
|
||||
}, [timer, haptics, workout, id])
|
||||
|
||||
// Stop workout
|
||||
const stopWorkout = useCallback(() => {
|
||||
@@ -274,6 +330,15 @@ export default function PlayerScreen() {
|
||||
// Complete workout - go to celebration screen
|
||||
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,
|
||||
})
|
||||
}
|
||||
if (workout) {
|
||||
addWorkoutResult({
|
||||
id: Date.now().toString(),
|
||||
@@ -301,30 +366,12 @@ export default function PlayerScreen() {
|
||||
|
||||
// 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()
|
||||
Animated.spring(timerScaleAnim, {
|
||||
toValue: 1,
|
||||
friction: 6,
|
||||
tension: 100,
|
||||
useNativeDriver: true,
|
||||
}).start()
|
||||
}, [])
|
||||
|
||||
// Phase change animation + audio
|
||||
@@ -351,28 +398,18 @@ export default function PlayerScreen() {
|
||||
}
|
||||
}, [timer.timeRemaining])
|
||||
|
||||
const glowOpacity = glowAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [0.3, 0.6],
|
||||
})
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar hidden />
|
||||
|
||||
{/* Background gradient */}
|
||||
<LinearGradient
|
||||
colors={[DARK.BASE, DARK.SURFACE]}
|
||||
colors={[colors.bg.base, colors.bg.surface]}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
|
||||
{/* Phase glow */}
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.phaseGlow,
|
||||
{ opacity: glowOpacity, backgroundColor: phaseColor },
|
||||
]}
|
||||
/>
|
||||
{/* Phase background color */}
|
||||
<View style={[styles.phaseBackground, { backgroundColor: phaseColor }]} />
|
||||
|
||||
{/* Main content */}
|
||||
<Pressable style={styles.content} onPress={toggleControls}>
|
||||
@@ -380,12 +417,12 @@ export default function PlayerScreen() {
|
||||
{showControls && (
|
||||
<View style={[styles.header, { paddingTop: insets.top + SPACING[4] }]}>
|
||||
<Pressable onPress={stopWorkout} style={styles.closeButton}>
|
||||
<BlurView intensity={GLASS.BLUR_LIGHT} tint="dark" style={StyleSheet.absoluteFill} />
|
||||
<Ionicons name="close" size={24} color={TEXT.PRIMARY} />
|
||||
<BlurView intensity={colors.glass.blurLight} tint={colors.glass.blurTint} style={StyleSheet.absoluteFill} />
|
||||
<Ionicons name="close" size={24} color={colors.text.primary} />
|
||||
</Pressable>
|
||||
<View style={styles.headerCenter}>
|
||||
<Text style={styles.workoutTitle}>{workout?.title ?? 'Workout'}</Text>
|
||||
<Text style={styles.workoutTrainer}>with {trainer?.name ?? 'Coach'}</Text>
|
||||
<Text style={styles.workoutTrainer}>{t('durationLevel', { duration: workout?.duration ?? 0, level: t(`levels.${(workout?.level ?? 'beginner').toLowerCase()}`) })}</Text>
|
||||
</View>
|
||||
<View style={styles.closeButton} />
|
||||
</View>
|
||||
@@ -418,20 +455,20 @@ export default function PlayerScreen() {
|
||||
{/* Complete state */}
|
||||
{timer.isComplete && (
|
||||
<View style={styles.completeContainer}>
|
||||
<Text style={styles.completeTitle}>Workout Complete!</Text>
|
||||
<Text style={styles.completeSubtitle}>Great job!</Text>
|
||||
<Text style={styles.completeTitle}>{t('screens:player.workoutComplete')}</Text>
|
||||
<Text style={styles.completeSubtitle}>{t('screens:player.greatJob')}</Text>
|
||||
<View style={styles.completeStats}>
|
||||
<View style={styles.completeStat}>
|
||||
<Text style={styles.completeStatValue}>{timer.totalRounds}</Text>
|
||||
<Text style={styles.completeStatLabel}>Rounds</Text>
|
||||
<Text style={styles.completeStatLabel}>{t('screens:player.rounds')}</Text>
|
||||
</View>
|
||||
<View style={styles.completeStat}>
|
||||
<Text style={styles.completeStatValue}>{timer.calories}</Text>
|
||||
<Text style={styles.completeStatLabel}>Calories</Text>
|
||||
<Text style={styles.completeStatLabel}>{t('screens:player.calories')}</Text>
|
||||
</View>
|
||||
<View style={styles.completeStat}>
|
||||
<Text style={styles.completeStatValue}>{workout?.duration ?? 4}</Text>
|
||||
<Text style={styles.completeStatLabel}>Minutes</Text>
|
||||
<Text style={styles.completeStatLabel}>{t('screens:player.minutes')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -484,7 +521,7 @@ export default function PlayerScreen() {
|
||||
{/* Burn Bar */}
|
||||
{showControls && timer.isRunning && !timer.isComplete && (
|
||||
<View style={[styles.burnBarContainer, { bottom: insets.bottom + 140 }]}>
|
||||
<BlurView intensity={GLASS.BLUR_MEDIUM} tint="dark" style={StyleSheet.absoluteFill} />
|
||||
<BlurView intensity={colors.glass.blurMedium} tint={colors.glass.blurTint} style={StyleSheet.absoluteFill} />
|
||||
<BurnBar currentCalories={timer.calories} avgCalories={workout?.calories ?? 45} />
|
||||
</View>
|
||||
)}
|
||||
@@ -497,260 +534,250 @@ export default function PlayerScreen() {
|
||||
// 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',
|
||||
},
|
||||
function createTimerStyles(colors: ThemeColors) {
|
||||
return StyleSheet.create({
|
||||
timerRingContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
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: colors.text.primary,
|
||||
},
|
||||
roundIndicator: {
|
||||
marginTop: SPACING[2],
|
||||
},
|
||||
roundText: {
|
||||
...TYPOGRAPHY.BODY,
|
||||
color: colors.text.tertiary,
|
||||
},
|
||||
roundCurrent: {
|
||||
color: colors.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,
|
||||
},
|
||||
// Exercise
|
||||
exerciseDisplay: {
|
||||
alignItems: 'center',
|
||||
marginTop: SPACING[6],
|
||||
paddingHorizontal: SPACING[6],
|
||||
},
|
||||
currentExerciseLabel: {
|
||||
...TYPOGRAPHY.CAPTION_1,
|
||||
color: colors.text.tertiary,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
currentExercise: {
|
||||
...TYPOGRAPHY.TITLE_1,
|
||||
color: colors.text.primary,
|
||||
textAlign: 'center',
|
||||
marginTop: SPACING[1],
|
||||
},
|
||||
nextExerciseContainer: {
|
||||
flexDirection: 'row',
|
||||
marginTop: SPACING[2],
|
||||
},
|
||||
nextExerciseLabel: {
|
||||
...TYPOGRAPHY.BODY,
|
||||
color: colors.text.tertiary,
|
||||
},
|
||||
nextExercise: {
|
||||
...TYPOGRAPHY.BODY,
|
||||
color: BRAND.PRIMARY,
|
||||
},
|
||||
|
||||
// Controls
|
||||
controlButton: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
controlButtonBg: {
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 100,
|
||||
},
|
||||
// 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',
|
||||
},
|
||||
})
|
||||
// Burn Bar
|
||||
burnBar: {},
|
||||
burnBarHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: SPACING[2],
|
||||
},
|
||||
burnBarLabel: {
|
||||
...TYPOGRAPHY.CAPTION_1,
|
||||
color: colors.text.tertiary,
|
||||
},
|
||||
burnBarValue: {
|
||||
...TYPOGRAPHY.CALLOUT,
|
||||
color: BRAND.PRIMARY,
|
||||
fontWeight: '600',
|
||||
},
|
||||
burnBarTrack: {
|
||||
height: 6,
|
||||
backgroundColor: colors.border.glass,
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
burnBarFill: {
|
||||
height: '100%',
|
||||
backgroundColor: BRAND.PRIMARY,
|
||||
borderRadius: 3,
|
||||
},
|
||||
burnBarAvg: {
|
||||
position: 'absolute',
|
||||
top: -2,
|
||||
width: 2,
|
||||
height: 10,
|
||||
backgroundColor: colors.text.tertiary,
|
||||
},
|
||||
burnBarAvgLabel: {
|
||||
...TYPOGRAPHY.CAPTION_2,
|
||||
color: colors.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,
|
||||
},
|
||||
function createStyles(colors: ThemeColors) {
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.bg.base,
|
||||
},
|
||||
phaseBackground: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
opacity: 0.15,
|
||||
},
|
||||
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,
|
||||
},
|
||||
// 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: colors.border.glass,
|
||||
},
|
||||
headerCenter: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
workoutTitle: {
|
||||
...TYPOGRAPHY.HEADLINE,
|
||||
color: colors.text.primary,
|
||||
},
|
||||
workoutTrainer: {
|
||||
...TYPOGRAPHY.CAPTION_1,
|
||||
color: colors.text.tertiary,
|
||||
},
|
||||
|
||||
// Timer
|
||||
timerContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: SPACING[8],
|
||||
},
|
||||
// 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],
|
||||
},
|
||||
// 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],
|
||||
},
|
||||
// Burn Bar
|
||||
burnBarContainer: {
|
||||
position: 'absolute',
|
||||
left: SPACING[4],
|
||||
right: SPACING[4],
|
||||
height: 72,
|
||||
borderRadius: RADIUS.LG,
|
||||
overflow: 'hidden',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border.glass,
|
||||
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,
|
||||
},
|
||||
})
|
||||
// Complete
|
||||
completeContainer: {
|
||||
alignItems: 'center',
|
||||
marginTop: SPACING[8],
|
||||
},
|
||||
completeTitle: {
|
||||
...TYPOGRAPHY.LARGE_TITLE,
|
||||
color: colors.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: colors.text.primary,
|
||||
},
|
||||
completeStatLabel: {
|
||||
...TYPOGRAPHY.CAPTION_1,
|
||||
color: colors.text.tertiary,
|
||||
marginTop: SPACING[1],
|
||||
},
|
||||
doneButton: {
|
||||
width: 200,
|
||||
height: 56,
|
||||
borderRadius: RADIUS.GLASS_BUTTON,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
...colors.shadow.BRAND_GLOW,
|
||||
},
|
||||
doneButtonText: {
|
||||
...TYPOGRAPHY.BUTTON_MEDIUM,
|
||||
color: colors.text.primary,
|
||||
letterSpacing: 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user