import React, { useState, useRef, useEffect } from 'react'; import { View, TouchableOpacity, StyleSheet, Text, Dimensions, Modal, Pressable, StatusBar, Platform, ScrollView, Animated } from 'react-native'; import Video from 'react-native-video'; import { Ionicons } from '@expo/vector-icons'; import { Slider } from 'react-native-awesome-slider'; import { LinearGradient } from 'expo-linear-gradient'; import { useSharedValue, runOnJS, withTiming } from 'react-native-reanimated'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; import { RootStackParamList } from '../navigation/AppNavigator'; // Remove Gesture Handler imports // import { PinchGestureHandler, State, PinchGestureHandlerGestureEvent } from 'react-native-gesture-handler'; // Import for navigation bar hiding import { NativeModules } from 'react-native'; // Import immersive mode package import RNImmersiveMode from 'react-native-immersive-mode'; // Import screen orientation lock import * as ScreenOrientation from 'expo-screen-orientation'; // Import storage service for progress tracking import { storageService } from '../services/storageService'; import { logger } from '../utils/logger'; import AsyncStorage from '@react-native-async-storage/async-storage'; // Constants for resume preferences - add after type definitions const RESUME_PREF_KEY = '@video_resume_preference'; const RESUME_PREF = { ALWAYS_ASK: 'always_ask', ALWAYS_RESUME: 'always_resume', ALWAYS_START_OVER: 'always_start_over' }; // Define the TrackPreferenceType for audio/text tracks type TrackPreferenceType = 'system' | 'disabled' | 'title' | 'language' | 'index'; // Define the SelectedTrack type for audio/text tracks interface SelectedTrack { type: TrackPreferenceType; value?: string | number; // value is optional for 'system' and 'disabled' } interface VideoPlayerProps { uri: string; title?: string; season?: number; episode?: number; episodeTitle?: string; quality?: string; year?: number; streamProvider?: string; id?: string; type?: string; episodeId?: string; } // Match the react-native-video AudioTrack type interface AudioTrack { index: number; title?: string; language?: string; bitrate?: number; type?: string; selected?: boolean; } // Define TextTrack interface based on react-native-video expected structure interface TextTrack { index: number; title?: string; language?: string; type?: string | null; // Adjusting type based on linter error } // Define the possible resize modes type ResizeModeType = 'contain' | 'cover' | 'stretch' | 'none'; const resizeModes: ResizeModeType[] = ['contain', 'cover', 'stretch']; // Add language code to name mapping const languageMap: {[key: string]: string} = { 'en': 'English', 'eng': 'English', 'es': 'Spanish', 'spa': 'Spanish', 'fr': 'French', 'fre': 'French', 'de': 'German', 'ger': 'German', 'it': 'Italian', 'ita': 'Italian', 'ja': 'Japanese', 'jpn': 'Japanese', 'ko': 'Korean', 'kor': 'Korean', 'zh': 'Chinese', 'chi': 'Chinese', 'ru': 'Russian', 'rus': 'Russian', 'pt': 'Portuguese', 'por': 'Portuguese', 'hi': 'Hindi', 'hin': 'Hindi', 'ar': 'Arabic', 'ara': 'Arabic', 'nl': 'Dutch', 'dut': 'Dutch', 'sv': 'Swedish', 'swe': 'Swedish', 'no': 'Norwegian', 'nor': 'Norwegian', 'fi': 'Finnish', 'fin': 'Finnish', 'da': 'Danish', 'dan': 'Danish', 'pl': 'Polish', 'pol': 'Polish', 'tr': 'Turkish', 'tur': 'Turkish', 'cs': 'Czech', 'cze': 'Czech', 'hu': 'Hungarian', 'hun': 'Hungarian', 'el': 'Greek', 'gre': 'Greek', 'th': 'Thai', 'tha': 'Thai', 'vi': 'Vietnamese', 'vie': 'Vietnamese', }; // Function to format language code to readable name const formatLanguage = (code?: string): string => { if (!code) return 'Unknown'; const normalized = code.toLowerCase(); return languageMap[normalized] || code.toUpperCase(); }; const VideoPlayer: React.FC = () => { const navigation = useNavigation(); const route = useRoute>(); // Extract props from route.params const { uri, title = 'Episode Name', season, episode, episodeTitle, quality, year, streamProvider, id, type, episodeId } = route.params; // Log received props for debugging logger.log("[VideoPlayer] Received props:", { uri, title, season, episode, episodeTitle, quality, year, streamProvider, id, type, episodeId }); const [paused, setPaused] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [showControls, setShowControls] = useState(true); const [playbackSpeed, setPlaybackSpeed] = useState(1); const [audioTracks, setAudioTracks] = useState([]); const [selectedAudioTrack, setSelectedAudioTrack] = useState(null); const [textTracks, setTextTracks] = useState([]); const [selectedTextTrack, setSelectedTextTrack] = useState({ type: 'disabled' }); const [resizeMode, setResizeMode] = useState('contain'); // State for resize mode const videoRef = useRef(null); const progress = useSharedValue(0); const min = useSharedValue(0); const max = useSharedValue(duration); const [showAudioModal, setShowAudioModal] = useState(false); const [showSubtitleModal, setShowSubtitleModal] = useState(false); // Add state for tracking initial position to seek to const [initialPosition, setInitialPosition] = useState(null); const [progressSaveInterval, setProgressSaveInterval] = useState(null); const [isInitialSeekComplete, setIsInitialSeekComplete] = useState(false); // Add state for showing resume overlay const [showResumeOverlay, setShowResumeOverlay] = useState(false); const [resumePosition, setResumePosition] = useState(null); // Add state for remembering choice const [rememberChoice, setRememberChoice] = useState(false); const [resumePreference, setResumePreference] = useState(null); // Add animated value for controls opacity const fadeAnim = useRef(new Animated.Value(1)).current; // Add buffered state const [buffered, setBuffered] = useState(0); // Lock screen to landscape when component mounts useEffect(() => { const lockToLandscape = async () => { await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); }; // Lock to landscape lockToLandscape(); // Enable immersive mode when component mounts enableImmersiveMode(); // Restore screen orientation and disable immersive mode when component unmounts return () => { const unlockOrientation = async () => { await ScreenOrientation.unlockAsync(); }; unlockOrientation(); disableImmersiveMode(); }; }, []); // Load saved watch progress on mount useEffect(() => { const loadWatchProgress = async () => { if (id && type) { try { logger.log(`[VideoPlayer] Checking for saved progress with id=${id}, type=${type}, episodeId=${episodeId || 'none'}`); const savedProgress = await storageService.getWatchProgress(id, type, episodeId); if (savedProgress) { logger.log(`[VideoPlayer] Found saved progress:`, savedProgress); if (savedProgress.currentTime > 0) { // Only auto-resume if less than 95% watched (not effectively complete) const progressPercent = (savedProgress.currentTime / savedProgress.duration) * 100; logger.log(`[VideoPlayer] Progress percent: ${progressPercent.toFixed(2)}%`); if (progressPercent < 95) { logger.log(`[VideoPlayer] Setting initial position to ${savedProgress.currentTime}`); // Set resume position setResumePosition(savedProgress.currentTime); // Check for saved preference const pref = await AsyncStorage.getItem(RESUME_PREF_KEY); if (pref === RESUME_PREF.ALWAYS_RESUME) { setInitialPosition(savedProgress.currentTime); logger.log(`[VideoPlayer] Auto-resuming based on saved preference`); } else if (pref === RESUME_PREF.ALWAYS_START_OVER) { setInitialPosition(0); logger.log(`[VideoPlayer] Auto-starting from beginning based on saved preference`); } else { // Only show resume overlay if no preference or ALWAYS_ASK setShowResumeOverlay(true); } } else { logger.log(`[VideoPlayer] Progress >= 95%, starting from beginning`); } } } else { logger.log(`[VideoPlayer] No saved progress found`); } } catch (error) { logger.error('[VideoPlayer] Error loading watch progress:', error); } } else { logger.log(`[VideoPlayer] Missing id or type, can't load progress. id=${id}, type=${type}`); } }; loadWatchProgress(); }, [id, type, episodeId]); // Set up interval to save watch progress periodically (every 5 seconds) useEffect(() => { if (id && type && !paused && duration > 0) { // Clear any existing interval if (progressSaveInterval) { clearInterval(progressSaveInterval); } // Set up new interval to save progress const interval = setInterval(() => { saveWatchProgress(); }, 5000); setProgressSaveInterval(interval); // Clean up interval on pause or unmount return () => { clearInterval(interval); setProgressSaveInterval(null); }; } }, [id, type, paused, currentTime, duration]); // Save progress one more time when component unmounts useEffect(() => { return () => { if (id && type && duration > 0) { saveWatchProgress(); } }; }, [id, type, currentTime, duration]); // Function to save watch progress const saveWatchProgress = async () => { if (id && type && currentTime > 0 && duration > 0) { const progress = { currentTime, duration, lastUpdated: Date.now() }; try { await storageService.setWatchProgress(id, type, progress, episodeId); logger.log(`[VideoPlayer] Saved progress: ${currentTime.toFixed(1)}/${duration.toFixed(1)} (${((currentTime/duration)*100).toFixed(1)}%)`); } catch (error) { logger.error('[VideoPlayer] Error saving watch progress:', error); } } else { logger.log(`[VideoPlayer] Cannot save progress: id=${id}, type=${type}, currentTime=${currentTime}, duration=${duration}`); } }; useEffect(() => { max.value = duration; }, [duration]); const formatTime = (seconds: number) => { const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); const secs = Math.floor(seconds % 60); if (hours > 0) { return `${hours}:${mins < 10 ? '0' : ''}${mins}:${secs < 10 ? '0' : ''}${secs}`; } else { return `${mins}:${secs < 10 ? '0' : ''}${secs}`; } }; const onSliderValueChange = (value: number) => { if (videoRef.current) { const newTime = Math.floor(value); videoRef.current.seek(newTime); setCurrentTime(newTime); progress.value = newTime; } }; const togglePlayback = () => { setPaused(!paused); }; const skip = (seconds: number) => { if (videoRef.current) { const newTime = Math.max(0, Math.min(currentTime + seconds, duration)); videoRef.current.seek(newTime); setCurrentTime(newTime); progress.value = newTime; } }; const onProgress = (data: { currentTime: number }) => { setCurrentTime(data.currentTime); progress.value = data.currentTime; }; const onLoad = (data: { duration: number }) => { setDuration(data.duration); max.value = data.duration; logger.log(`[VideoPlayer] Video loaded with duration: ${data.duration}`); // If we have an initial position to seek to, do it now if (initialPosition !== null && !isInitialSeekComplete && videoRef.current) { logger.log(`[VideoPlayer] Will seek to saved position: ${initialPosition}`); // Seek immediately with a small delay setTimeout(() => { if (videoRef.current) { try { videoRef.current.seek(initialPosition); setCurrentTime(initialPosition); progress.value = initialPosition; setIsInitialSeekComplete(true); logger.log(`[VideoPlayer] Successfully seeked to saved position: ${initialPosition}`); } catch (error) { logger.error('[VideoPlayer] Error seeking to saved position:', error); } } else { logger.error('[VideoPlayer] videoRef is no longer valid when attempting to seek'); } }, 1000); // Increase delay to ensure video is fully loaded } else { if (initialPosition === null) { logger.log(`[VideoPlayer] No initial position to seek to`); } else if (isInitialSeekComplete) { logger.log(`[VideoPlayer] Initial seek already completed`); } else { logger.log(`[VideoPlayer] videoRef not available for seeking`); } } }; const onAudioTracks = (data: { audioTracks: AudioTrack[] }) => { const tracks = data.audioTracks || []; setAudioTracks(tracks); logger.log(`[VideoPlayer] Available audio tracks:`, tracks); }; const onTextTracks = (e: Readonly<{ textTracks: TextTrack[] }>) => { const tracks = e.textTracks || []; setTextTracks(tracks); logger.log(`[VideoPlayer] Available subtitle tracks:`, tracks); }; // Toggle through aspect ratio modes const cycleAspectRatio = () => { const currentIndex = resizeModes.indexOf(resizeMode); const nextIndex = (currentIndex + 1) % resizeModes.length; logger.log(`[VideoPlayer] Changing aspect ratio from ${resizeMode} to ${resizeModes[nextIndex]}`); setResizeMode(resizeModes[nextIndex]); }; // Function to enable immersive mode const enableImmersiveMode = () => { StatusBar.setHidden(true); if (Platform.OS === 'android') { // Full immersive mode - hides both status and navigation bars // Use setBarMode with 'FullSticky' mode to hide all bars with sticky behavior RNImmersiveMode.setBarMode('FullSticky'); // Alternative: if you want to use fullLayout method (which is in the TypeScript definition) RNImmersiveMode.fullLayout(true); } }; // Function to disable immersive mode const disableImmersiveMode = () => { StatusBar.setHidden(false); if (Platform.OS === 'android') { // Restore normal mode using setBarMode RNImmersiveMode.setBarMode('Normal'); // Alternative: disable fullLayout RNImmersiveMode.fullLayout(false); } }; // Function to handle closing the video player const handleClose = () => { // First unlock the screen orientation const unlockOrientation = async () => { await ScreenOrientation.unlockAsync(); }; unlockOrientation(); // Disable immersive mode disableImmersiveMode(); // Navigate back navigation.goBack(); }; // Add debug logs for modal visibility useEffect(() => { if (showAudioModal) { logger.log("[VideoPlayer] Audio modal should be visible now"); logger.log("[VideoPlayer] Available audio tracks:", audioTracks); } }, [showAudioModal, audioTracks]); useEffect(() => { if (showSubtitleModal) { logger.log("[VideoPlayer] Subtitle modal should be visible now"); logger.log("[VideoPlayer] Available text tracks:", textTracks); } }, [showSubtitleModal, textTracks]); // Attempt to seek once videoRef is available useEffect(() => { if (initialPosition !== null && !isInitialSeekComplete && videoRef.current) { logger.log(`[VideoPlayer] videoRef is now available, attempting to seek to: ${initialPosition}`); try { videoRef.current.seek(initialPosition); setCurrentTime(initialPosition); progress.value = initialPosition; setIsInitialSeekComplete(true); logger.log(`[VideoPlayer] Successfully seeked to position: ${initialPosition}`); } catch (error) { logger.error('[VideoPlayer] Error seeking to position on ref available:', error); } } }, [videoRef.current, initialPosition, isInitialSeekComplete]); // Load resume preference on mount useEffect(() => { const loadResumePreference = async () => { try { const pref = await AsyncStorage.getItem(RESUME_PREF_KEY); if (pref) { setResumePreference(pref); logger.log(`[VideoPlayer] Loaded resume preference: ${pref}`); // If user has a preference, apply it automatically if (pref === RESUME_PREF.ALWAYS_RESUME && resumePosition !== null) { setShowResumeOverlay(false); setInitialPosition(resumePosition); logger.log(`[VideoPlayer] Auto-resuming based on saved preference`); } else if (pref === RESUME_PREF.ALWAYS_START_OVER) { setShowResumeOverlay(false); setInitialPosition(0); logger.log(`[VideoPlayer] Auto-starting from beginning based on saved preference`); } } } catch (error) { logger.error('[VideoPlayer] Error loading resume preference:', error); } }; loadResumePreference(); }, [resumePosition]); // Reset resume preference const resetResumePreference = async () => { try { await AsyncStorage.removeItem(RESUME_PREF_KEY); setResumePreference(null); logger.log(`[VideoPlayer] Reset resume preference`); } catch (error) { logger.error('[VideoPlayer] Error resetting resume preference:', error); } }; // Handle resume from overlay - modified to save preference const handleResume = async () => { if (resumePosition !== null && videoRef.current) { logger.log(`[VideoPlayer] Resuming from ${resumePosition}`); // Save preference if remember choice is checked if (rememberChoice) { try { await AsyncStorage.setItem(RESUME_PREF_KEY, RESUME_PREF.ALWAYS_RESUME); logger.log(`[VideoPlayer] Saved resume preference: ${RESUME_PREF.ALWAYS_RESUME}`); } catch (error) { logger.error('[VideoPlayer] Error saving resume preference:', error); } } // Set initial position to trigger seek setInitialPosition(resumePosition); // Hide overlay setShowResumeOverlay(false); } }; // Handle start from beginning - modified to save preference const handleStartFromBeginning = async () => { logger.log(`[VideoPlayer] Starting from beginning`); // Save preference if remember choice is checked if (rememberChoice) { try { await AsyncStorage.setItem(RESUME_PREF_KEY, RESUME_PREF.ALWAYS_START_OVER); logger.log(`[VideoPlayer] Saved resume preference: ${RESUME_PREF.ALWAYS_START_OVER}`); } catch (error) { logger.error('[VideoPlayer] Error saving resume preference:', error); } } // Hide overlay setShowResumeOverlay(false); // Set initial position to 0 setInitialPosition(0); // Make sure we seek to beginning if (videoRef.current) { videoRef.current.seek(0); setCurrentTime(0); progress.value = 0; } }; // Update the showControls logic to include animation const toggleControls = () => { // Start fade animation Animated.timing(fadeAnim, { toValue: showControls ? 0 : 1, duration: 300, useNativeDriver: true, }).start(); // Update state setShowControls(!showControls); }; // Add onBuffer handler to Video component const onBuffer = ({ isBuffering }: { isBuffering: boolean }) => { // You can use this to show a loading indicator if needed logger.log(`[VideoPlayer] Buffering: ${isBuffering}`); }; // Add onProgress handler to track buffered data const onLoadStart = () => { setBuffered(0); }; const handleProgress = (data: { currentTime: number, playableDuration: number, seekableDuration?: number }) => { setCurrentTime(data.currentTime); progress.value = data.currentTime; // Ensure playableDuration is always at least equal to currentTime const effectivePlayableDuration = Math.max(data.currentTime, data.playableDuration); setBuffered(effectivePlayableDuration); // Calculate buffer ahead (cannot be negative) const bufferAhead = Math.max(0, effectivePlayableDuration - data.currentTime); const bufferPercentage = ((effectivePlayableDuration / (duration || 1)) * 100); // Add detailed buffer logging logger.log(`[VideoPlayer] Buffer Status: Current Time: ${data.currentTime.toFixed(2)}s Playable Duration: ${effectivePlayableDuration.toFixed(2)}s Buffered Ahead: ${bufferAhead.toFixed(2)}s Seekable Duration: ${data.seekableDuration?.toFixed(2) || 'N/A'}s Buffer Percentage: ${bufferPercentage.toFixed(1)}% `); }; // Add onError handler const handleError = (error: any) => { logger.error('[VideoPlayer] Playback Error:', error); // Optionally, you could show an error message to the user here }; return ( {/* Audio Selection Modal - Updated language display */} {showAudioModal && ( Audio setShowAudioModal(false)} > {audioTracks.length > 0 ? audioTracks.map(track => ( { setSelectedAudioTrack(track.index); setShowAudioModal(false); }} > {formatLanguage(track.language) || track.title || `Track ${track.index + 1}`} {(track.title && track.language) && ( {track.title} )} {track.type && {track.type}} {selectedAudioTrack === track.index && ( )} )) : ( No audio tracks available )} )} {/* Subtitle Selection Modal - Updated language display */} {showSubtitleModal && ( Subtitles setShowSubtitleModal(false)} > {/* Off option with improved design */} { setSelectedTextTrack({ type: 'disabled' }); setShowSubtitleModal(false); }} > Off {selectedTextTrack?.type === 'disabled' && ( )} {/* Available subtitle tracks with improved design */} {textTracks.length > 0 ? textTracks.map(track => ( { setSelectedTextTrack({ type: 'index', value: track.index }); setShowSubtitleModal(false); }} > {formatLanguage(track.language) || track.title || `Subtitle ${track.index + 1}`} {(track.title && track.language) && ( {track.title} )} {track.type && {track.type}} {selectedTextTrack?.type === 'index' && selectedTextTrack?.value === track.index && ( )} )) : ( No subtitle tracks available )} )} ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#000', }, videoContainer: { flex: 1, }, video: { flex: 1, }, controlsContainer: { ...StyleSheet.absoluteFillObject, justifyContent: 'space-between', }, topGradient: { paddingTop: Platform.OS === 'ios' ? 50 : 20, // Adjust top padding for safe area paddingHorizontal: 20, paddingBottom: 10, // Add some padding at the bottom of the gradient }, bottomGradient: { paddingBottom: Platform.OS === 'ios' ? 30 : 20, paddingHorizontal: 20, paddingTop: 20, }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', // Align items to the top }, // Styles for the title section and metadata titleSection: { flex: 1, // Allow title section to take available space marginRight: 10, // Add margin to avoid overlap with close button }, title: { color: 'white', fontSize: 18, fontWeight: 'bold', }, episodeInfo: { color: 'rgba(255, 255, 255, 0.9)', fontSize: 14, marginTop: 3, }, metadataRow: { flexDirection: 'row', alignItems: 'center', marginTop: 5, flexWrap: 'wrap', // Allow items to wrap if needed }, metadataText: { color: 'rgba(255, 255, 255, 0.7)', fontSize: 12, marginRight: 8, }, qualityBadge: { backgroundColor: '#E50914', paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, marginRight: 8, }, qualityText: { color: 'white', fontSize: 10, fontWeight: 'bold', }, providerText: { color: 'rgba(255, 255, 255, 0.7)', fontSize: 12, fontStyle: 'italic', // Italicize provider text }, closeButton: { padding: 8, }, controls: { position: 'absolute', flexDirection: 'row', justifyContent: 'center', alignItems: 'center', gap: 40, left: 0, right: 0, top: '50%', transform: [{ translateY: -30 }], // Half the height of play button to center it perfectly zIndex: 1000, }, playButton: { justifyContent: 'center', alignItems: 'center', padding: 10, }, skipButton: { alignItems: 'center', justifyContent: 'center', }, skipText: { color: 'white', fontSize: 12, marginTop: 2, }, bottomControls: { gap: 12, }, sliderContainer: { position: 'absolute', bottom: 55, // Moved closer to bottom buttons left: 0, right: 0, paddingHorizontal: 20, zIndex: 1000, }, sliderBackground: { position: 'absolute', left: 0, right: 0, height: 3, backgroundColor: 'rgba(255, 255, 255, 0.2)', borderRadius: 1.5, overflow: 'hidden', marginHorizontal: 20, top: 13.5, // Center with the slider thumb }, bufferProgress: { position: 'absolute', left: 0, top: 0, bottom: 0, backgroundColor: 'rgba(255, 255, 255, 0.4)', }, slider: { width: '100%', height: 30, zIndex: 1, }, timeDisplay: { flexDirection: 'row', justifyContent: 'space-between', width: '100%', paddingHorizontal: 4, marginTop: -4, // Reduced space between slider and time marginBottom: 8, // Added space between time and buttons }, duration: { color: 'white', fontSize: 12, fontWeight: '500', }, bottomButtons: { flexDirection: 'row', justifyContent: 'space-around', alignItems: 'center', }, bottomButton: { flexDirection: 'row', alignItems: 'center', gap: 5, }, bottomButtonText: { color: 'white', fontSize: 12, }, modalOverlay: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0, 0, 0, 0.7)', }, modalContent: { width: '80%', maxHeight: '70%', backgroundColor: '#222', borderRadius: 10, overflow: 'hidden', zIndex: 1000, elevation: 5, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.8, shadowRadius: 5, }, modalHeader: { padding: 16, flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', borderBottomWidth: 1, borderBottomColor: '#333', }, modalTitle: { color: 'white', fontSize: 18, fontWeight: 'bold', }, trackList: { padding: 10, }, trackItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 15, borderRadius: 5, marginVertical: 5, }, selectedTrackItem: { backgroundColor: 'rgba(229, 9, 20, 0.2)', }, trackLabel: { color: 'white', fontSize: 16, }, noTracksText: { color: 'white', fontSize: 16, textAlign: 'center', padding: 20, }, // New simplified modal styles fullscreenOverlay: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.85)', justifyContent: 'center', alignItems: 'center', zIndex: 2000, }, enhancedModalContainer: { width: 300, maxHeight: '70%', backgroundColor: '#181818', borderRadius: 8, overflow: 'hidden', shadowColor: '#000', shadowOffset: { width: 0, height: 6 }, shadowOpacity: 0.4, shadowRadius: 10, elevation: 8, }, enhancedModalHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#333', }, enhancedModalTitle: { color: 'white', fontSize: 18, fontWeight: 'bold', }, enhancedCloseButton: { padding: 4, }, trackListScrollContainer: { maxHeight: 350, }, trackListContainer: { padding: 6, }, enhancedTrackItem: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', padding: 10, marginVertical: 2, borderRadius: 6, backgroundColor: '#222', }, trackInfoContainer: { flex: 1, marginRight: 8, }, trackPrimaryText: { color: 'white', fontSize: 14, fontWeight: '500', }, trackSecondaryText: { color: '#aaa', fontSize: 11, marginTop: 2, }, selectedIndicatorContainer: { width: 24, height: 24, borderRadius: 12, backgroundColor: 'rgba(229, 9, 20, 0.15)', justifyContent: 'center', alignItems: 'center', }, emptyStateContainer: { alignItems: 'center', justifyContent: 'center', padding: 20, }, emptyStateText: { color: '#888', fontSize: 14, marginTop: 8, textAlign: 'center', }, // Resume overlay styles resumeOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0, 0, 0, 0.7)', justifyContent: 'center', alignItems: 'center', zIndex: 1000, }, resumeContainer: { width: '80%', maxWidth: 500, borderRadius: 12, padding: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 6, elevation: 8, }, resumeContent: { flexDirection: 'row', alignItems: 'center', marginBottom: 20, }, resumeIconContainer: { marginRight: 16, width: 50, height: 50, borderRadius: 25, backgroundColor: 'rgba(229, 9, 20, 0.2)', justifyContent: 'center', alignItems: 'center', }, resumeTextContainer: { flex: 1, }, resumeTitle: { color: 'white', fontSize: 20, fontWeight: 'bold', marginBottom: 4, }, resumeInfo: { color: 'rgba(255, 255, 255, 0.9)', fontSize: 14, }, resumeProgressContainer: { marginTop: 12, }, resumeProgressBar: { height: 4, backgroundColor: 'rgba(255, 255, 255, 0.2)', borderRadius: 2, overflow: 'hidden', marginBottom: 6, }, resumeProgressFill: { height: '100%', backgroundColor: '#E50914', }, resumeTimeText: { color: 'rgba(255,255,255,0.7)', fontSize: 12, }, resumeButtons: { flexDirection: 'row', justifyContent: 'flex-end', width: '100%', gap: 12, }, resumeButton: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, paddingHorizontal: 16, borderRadius: 6, backgroundColor: 'rgba(255, 255, 255, 0.15)', minWidth: 110, justifyContent: 'center', }, buttonIcon: { marginRight: 6, }, resumeButtonText: { color: 'white', fontWeight: 'bold', fontSize: 14, }, resumeFromButton: { backgroundColor: '#E50914', }, rememberChoiceContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16, paddingHorizontal: 2, }, checkboxContainer: { flexDirection: 'row', alignItems: 'center', }, checkbox: { width: 18, height: 18, borderRadius: 3, borderWidth: 2, borderColor: 'rgba(255, 255, 255, 0.5)', marginRight: 8, justifyContent: 'center', alignItems: 'center', }, checkboxChecked: { backgroundColor: '#E50914', borderColor: '#E50914', }, rememberChoiceText: { color: 'rgba(255, 255, 255, 0.8)', fontSize: 14, }, resetPreferenceButton: { padding: 4, }, resetPreferenceText: { color: '#E50914', fontSize: 12, fontWeight: 'bold', }, }); export default VideoPlayer;