refactor screens, navigation & player for new architecture
Simplify Home, Activity, Profile, Complete, Player, and Program screens to work with the new Supabase-driven data layer. Update root and tab layouts. Add Settings, Terms, and Zone routes. Add OfflineBanner component and progressStore. Update all player sub-components to use the refreshed design system tokens.
This commit is contained in:
@@ -1,601 +1,82 @@
|
||||
/**
|
||||
* TabataFit Player Screen
|
||||
* Thin orchestrator — all UI extracted to src/features/player/
|
||||
* FORCE DARK — always uses darkColors regardless of system theme
|
||||
* Loads a WorkoutProgram from Supabase and renders the Tabata player.
|
||||
*/
|
||||
|
||||
import React, { useRef, useEffect, useCallback, useState } from 'react'
|
||||
import React from 'react'
|
||||
import { View, Text } from 'react-native'
|
||||
import { useLocalSearchParams } from 'expo-router'
|
||||
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 { 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 { isTabataSession, getTabataSessionById } from '@/src/shared/data/tabata'
|
||||
import { isWorkoutProgramId, parseWorkoutProgramId, fetchProgramById, workoutProgramToTabataSession } from '@/src/shared/data/workoutPrograms'
|
||||
isWorkoutProgramId,
|
||||
parseWorkoutProgramId,
|
||||
fetchProgramById,
|
||||
workoutProgramToTabataSession,
|
||||
} from '@/src/shared/data/workoutPrograms'
|
||||
import { TabataPlayerScreen } from '@/src/features/player/TabataPlayerScreen'
|
||||
import type { TabataSession } from '@/src/shared/types/program'
|
||||
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, 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 { GREEN, NAVY, BORDER_COLORS, TEXT } from '@/src/shared/constants/colors'
|
||||
|
||||
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 ─────────────────────────────────────────────────────────────
|
||||
import type { WorkoutProgram } from '@/src/shared/types/workoutProgram'
|
||||
import { NAVY, TEXT } from '@/src/shared/constants/colors'
|
||||
|
||||
export default function PlayerScreen() {
|
||||
useKeepAwake()
|
||||
const router = useRouter()
|
||||
const { id } = useLocalSearchParams<{ id: string }>()
|
||||
const sessionId = id ?? ''
|
||||
|
||||
// ─── Dispatch: Workout Program → Tabata session → Legacy workout ─
|
||||
const sessionId = id ?? '1'
|
||||
|
||||
if (isWorkoutProgramId(sessionId)) {
|
||||
return <WorkoutProgramPlayerScreen compositeId={sessionId} />
|
||||
if (!isWorkoutProgramId(sessionId)) {
|
||||
return <Message text="Programme invalide" />
|
||||
}
|
||||
|
||||
if (isTabataSession(sessionId)) {
|
||||
const session = getTabataSessionById(sessionId)
|
||||
if (session) {
|
||||
return <TabataPlayerScreen session={session} />
|
||||
}
|
||||
// Fallback to legacy if session not found
|
||||
}
|
||||
|
||||
return <LegacyPlayerScreen id={sessionId} />
|
||||
return <WorkoutProgramPlayerScreen compositeId={sessionId} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Workout Program player — async-loads a workout program from Supabase,
|
||||
* converts to TabataSession (3 tabata blocks), and renders TabataPlayerScreen.
|
||||
*/
|
||||
function WorkoutProgramPlayerScreen({ compositeId }: { compositeId: string }) {
|
||||
const [session, setSession] = React.useState<TabataSession | null | undefined>(undefined)
|
||||
const [state, setState] = React.useState<
|
||||
| { status: 'loading' }
|
||||
| { status: 'error' }
|
||||
| { status: 'ready'; session: TabataSession; program: WorkoutProgram }
|
||||
>({ status: 'loading' })
|
||||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false
|
||||
async function load() {
|
||||
const parsed = parseWorkoutProgramId(compositeId)
|
||||
if (!parsed) { if (!cancelled) setSession(null); return }
|
||||
if (!parsed) {
|
||||
if (!cancelled) setState({ status: 'error' })
|
||||
return
|
||||
}
|
||||
const program = await fetchProgramById(parsed.programId)
|
||||
if (cancelled) return
|
||||
if (!program) { setSession(null); return }
|
||||
setSession(workoutProgramToTabataSession(program))
|
||||
if (!program) {
|
||||
setState({ status: 'error' })
|
||||
return
|
||||
}
|
||||
setState({
|
||||
status: 'ready',
|
||||
session: workoutProgramToTabataSession(program),
|
||||
program,
|
||||
})
|
||||
}
|
||||
load()
|
||||
return () => { cancelled = true }
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [compositeId])
|
||||
|
||||
if (session === undefined) {
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: NAVY[900], justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Text style={{ color: TEXT.SECONDARY }}>Chargement...</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: NAVY[900], justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Text style={{ color: TEXT.SECONDARY }}>Programme non trouvé</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
return <TabataPlayerScreen session={session} />
|
||||
if (state.status === 'loading') return <Message text="Chargement..." />
|
||||
if (state.status === 'error') return <Message text="Programme non trouvé" />
|
||||
return <TabataPlayerScreen session={state.session} program={state.program} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Legacy player for original workout format
|
||||
*/
|
||||
function LegacyPlayerScreen({ id }: { id: string }) {
|
||||
const router = useRouter()
|
||||
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 && timer.phase !== 'PREP',
|
||||
})
|
||||
|
||||
const [showControls, setShowControls] = useState(true)
|
||||
const [heartRate, setHeartRate] = useState<number | null>(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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
function Message({ text }: { text: string }) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<StatusBar hidden />
|
||||
|
||||
{/* Background video or gradient fallback */}
|
||||
<VideoPlayer
|
||||
videoUrl={workout?.videoUrl}
|
||||
gradientColors={[colors.bg.base, colors.bg.surface]}
|
||||
mode="background"
|
||||
isPlaying={timer.isRunning && !timer.isPaused}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
|
||||
{/* Phase background tint */}
|
||||
<View style={[styles.phaseBg, { backgroundColor: phaseColor }]} />
|
||||
|
||||
{/* Main content */}
|
||||
<Pressable style={styles.content} onPress={toggleControls}>
|
||||
{/* Header */}
|
||||
{showControls && (
|
||||
<View style={[styles.header, { paddingTop: insets.top + SPACING[4] }]}>
|
||||
<Pressable onPress={stopWorkout} style={styles.closeBtn}>
|
||||
<View style={StyleSheet.absoluteFill} />
|
||||
<Icon name="xmark" size={24} tintColor={colors.text.primary} />
|
||||
</Pressable>
|
||||
<View style={styles.headerCenter}>
|
||||
<Text style={styles.title}>{workout?.title ?? 'Workout'}</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
{t('durationLevel', {
|
||||
duration: workout?.duration ?? 0,
|
||||
level: t(`levels.${(workout?.level ?? 'beginner').toLowerCase()}`),
|
||||
})}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.closeBtn} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Stats overlay — above timer ring */}
|
||||
{showControls && timer.isRunning && !timer.isComplete && (
|
||||
<View style={styles.statsContainer}>
|
||||
<StatsOverlay
|
||||
calories={timer.calories}
|
||||
heartRate={heartRate}
|
||||
elapsedRounds={timer.currentRound - 1}
|
||||
totalRounds={timer.totalRounds}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Timer ring + inner text */}
|
||||
<Animated.View
|
||||
style={[styles.timerContainer, { transform: [{ scale: timerScaleAnim }] }]}
|
||||
>
|
||||
<TimerRing progress={timer.progress} phase={timer.phase} />
|
||||
<View style={styles.timerInner}>
|
||||
<PhaseIndicator phase={timer.phase} />
|
||||
<Text selectable style={styles.timerTime}>
|
||||
{formatTime(timer.timeRemaining)}
|
||||
</Text>
|
||||
<RoundIndicator current={timer.currentRound} total={timer.totalRounds} />
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
{/* Exercise name + coach encouragement */}
|
||||
{!timer.isComplete && (
|
||||
<>
|
||||
<ExerciseDisplay
|
||||
exercise={timer.currentExercise}
|
||||
nextExercise={timer.nextExercise}
|
||||
/>
|
||||
<CoachEncouragement
|
||||
phase={timer.phase}
|
||||
currentRound={timer.currentRound}
|
||||
totalRounds={timer.totalRounds}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Complete state */}
|
||||
{timer.isComplete && (
|
||||
<View style={styles.completeSection}>
|
||||
<Text style={styles.completeTitle}>{t('screens:player.workoutComplete')}</Text>
|
||||
<Text style={[styles.completeSubtitle, { color: trainerColor }]}>{t('screens:player.greatJob')}</Text>
|
||||
<View style={styles.completeStats}>
|
||||
<View style={styles.completeStat}>
|
||||
<Text selectable style={styles.completeStatValue}>{timer.totalRounds}</Text>
|
||||
<Text style={styles.completeStatLabel}>{t('screens:player.rounds')}</Text>
|
||||
</View>
|
||||
<View style={styles.completeStat}>
|
||||
<Text selectable style={styles.completeStatValue}>{timer.calories}</Text>
|
||||
<Text style={styles.completeStatLabel}>{t('screens:player.calories')}</Text>
|
||||
</View>
|
||||
<View style={styles.completeStat}>
|
||||
<Text selectable style={styles.completeStatValue}>{workout?.duration ?? 4}</Text>
|
||||
<Text style={styles.completeStatLabel}>{t('screens:player.minutes')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Player controls */}
|
||||
{showControls && !timer.isComplete && (
|
||||
<View style={[styles.controls, { paddingBottom: insets.bottom + SPACING[6] }]}>
|
||||
<PlayerControls
|
||||
isRunning={timer.isRunning}
|
||||
isPaused={timer.isPaused}
|
||||
onStart={startTimer}
|
||||
onPause={() => { timer.pause(); haptics.selection(); track('workout_paused', { workout_id: workout?.id ?? id ?? '' }) }}
|
||||
onResume={() => { timer.resume(); haptics.selection(); track('workout_resumed', { workout_id: workout?.id ?? id ?? '' }) }}
|
||||
onStop={stopWorkout}
|
||||
onSkip={handleSkip}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Complete CTA */}
|
||||
{timer.isComplete && (
|
||||
<View style={[styles.controls, { paddingBottom: insets.bottom + SPACING[6] }]}>
|
||||
<Pressable style={styles.doneButton} onPress={completeWorkout}>
|
||||
<Text style={styles.doneButtonText}>{t('common:done')}</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Burn bar */}
|
||||
{showControls && timer.isRunning && !timer.isComplete && (
|
||||
<View style={[styles.burnBarContainer, { bottom: insets.bottom + 140 }]}>
|
||||
<BurnBar currentCalories={timer.calories} avgCalories={workout?.calories ?? 45} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Now Playing music pill */}
|
||||
{showControls && timer.isRunning && !timer.isComplete && (
|
||||
<View style={[styles.nowPlayingContainer, { bottom: insets.bottom + 220 }]}>
|
||||
<NowPlaying
|
||||
track={music.currentTrack}
|
||||
isReady={music.isReady}
|
||||
onSkipTrack={music.nextTrack}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: NAVY[900],
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Text style={{ color: TEXT.SECONDARY }}>{text}</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Styles ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: NAVY[900],
|
||||
},
|
||||
phaseBg: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
opacity: 0.15,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
|
||||
// Header
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: SPACING[4],
|
||||
},
|
||||
closeBtn: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: RADIUS.FULL,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
borderWidth: 1,
|
||||
borderColor: BORDER_COLORS.DIM,
|
||||
backgroundColor: NAVY[800],
|
||||
},
|
||||
headerCenter: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
...TYPOGRAPHY.HEADLINE,
|
||||
color: TEXT.PRIMARY,
|
||||
},
|
||||
subtitle: {
|
||||
...TYPOGRAPHY.CAPTION_1,
|
||||
color: TEXT.TERTIARY,
|
||||
},
|
||||
|
||||
// Stats overlay
|
||||
statsContainer: {
|
||||
marginTop: SPACING[4],
|
||||
marginHorizontal: SPACING[4],
|
||||
},
|
||||
|
||||
// Timer
|
||||
timerContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: SPACING[6],
|
||||
},
|
||||
timerInner: {
|
||||
position: 'absolute',
|
||||
alignItems: 'center',
|
||||
},
|
||||
timerTime: {
|
||||
...TYPOGRAPHY.TIMER_NUMBER,
|
||||
color: TEXT.PRIMARY,
|
||||
fontVariant: ['tabular-nums'],
|
||||
},
|
||||
|
||||
// Controls
|
||||
controls: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
alignItems: 'center',
|
||||
},
|
||||
|
||||
// Burn Bar
|
||||
burnBarContainer: {
|
||||
position: 'absolute',
|
||||
left: SPACING[4],
|
||||
right: SPACING[4],
|
||||
height: 72,
|
||||
borderRadius: RADIUS.LG,
|
||||
borderCurve: 'continuous',
|
||||
overflow: 'hidden',
|
||||
borderWidth: 1,
|
||||
borderColor: BORDER_COLORS.DIM,
|
||||
backgroundColor: NAVY[800],
|
||||
padding: SPACING[3],
|
||||
},
|
||||
|
||||
// Now Playing
|
||||
nowPlayingContainer: {
|
||||
position: 'absolute',
|
||||
left: SPACING[6],
|
||||
right: SPACING[6],
|
||||
},
|
||||
|
||||
// Complete
|
||||
completeSection: {
|
||||
alignItems: 'center',
|
||||
marginTop: SPACING[8],
|
||||
},
|
||||
completeTitle: {
|
||||
...TYPOGRAPHY.LARGE_TITLE,
|
||||
color: TEXT.PRIMARY,
|
||||
},
|
||||
completeSubtitle: {
|
||||
...TYPOGRAPHY.TITLE_3,
|
||||
marginTop: SPACING[1],
|
||||
},
|
||||
completeStats: {
|
||||
flexDirection: 'row',
|
||||
marginTop: SPACING[6],
|
||||
gap: SPACING[8],
|
||||
},
|
||||
completeStat: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
completeStatValue: {
|
||||
...TYPOGRAPHY.TITLE_1,
|
||||
color: TEXT.PRIMARY,
|
||||
fontVariant: ['tabular-nums'],
|
||||
},
|
||||
completeStatLabel: {
|
||||
...TYPOGRAPHY.CAPTION_1,
|
||||
color: TEXT.TERTIARY,
|
||||
marginTop: SPACING[1],
|
||||
},
|
||||
doneButton: {
|
||||
width: 200,
|
||||
height: 56,
|
||||
borderRadius: RADIUS.MD,
|
||||
borderCurve: 'continuous',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: GREEN[500],
|
||||
},
|
||||
doneButtonText: {
|
||||
...TYPOGRAPHY.BUTTON_MEDIUM,
|
||||
color: NAVY[900],
|
||||
letterSpacing: 1,
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user