From 45b2d4a124f7f2bd4198518d11ec9a8456e220e7 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sun, 8 Jun 2025 16:11:15 +0530 Subject: [PATCH] Enhance HomeScreen and StreamsScreen with landscape orientation locking and improved loading state management This update introduces the use of the expo-screen-orientation library to lock the screen orientation to landscape mode when navigating to the Player component from both HomeScreen and StreamsScreen. Additionally, it refines loading state management in StreamsScreen by implementing guards to prevent excessive re-renders and ensuring accurate provider status updates. The changes contribute to a smoother user experience during video playback and improved performance across the application. --- src/screens/HomeScreen.tsx | 45 +- src/screens/StreamsScreen.tsx | 245 ++++--- src/screens/VideoPlayer.tsx | 1226 ++++++++++++++++++++------------- 3 files changed, 939 insertions(+), 577 deletions(-) diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 5af6b374e..958b7731d 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; +import React, { useState, useEffect, useCallback, useRef, useMemo, useLayoutEffect } from 'react'; import { View, Text, @@ -16,7 +16,8 @@ import { Platform, Image, Modal, - Pressable + Pressable, + Alert } from 'react-native'; import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; @@ -60,6 +61,7 @@ import { SkeletonFeatured } from '../components/home/SkeletonLoaders'; import homeStyles, { sharedStyles } from '../styles/homeStyles'; import { useTheme } from '../contexts/ThemeContext'; import type { Theme } from '../contexts/ThemeContext'; +import * as ScreenOrientation from 'expo-screen-orientation'; // Define interfaces for our data interface Category { @@ -517,18 +519,37 @@ const HomeScreen = () => { navigation.navigate('Metadata', { id, type }); }, [navigation]); - const handlePlayStream = useCallback((stream: Stream) => { + const handlePlayStream = useCallback(async (stream: Stream) => { if (!featuredContent) return; - navigation.navigate('Player', { - uri: stream.url, - title: featuredContent.name, - year: featuredContent.year, - quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, - streamProvider: stream.name, - id: featuredContent.id, - type: featuredContent.type - }); + try { + // Lock orientation to landscape before navigation to prevent glitches + await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); + + // Small delay to ensure orientation is set before navigation + await new Promise(resolve => setTimeout(resolve, 100)); + + navigation.navigate('Player', { + uri: stream.url, + title: featuredContent.name, + year: featuredContent.year, + quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, + streamProvider: stream.name, + id: featuredContent.id, + type: featuredContent.type + }); + } catch (error) { + // Fallback: navigate anyway + navigation.navigate('Player', { + uri: stream.url, + title: featuredContent.name, + year: featuredContent.year, + quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, + streamProvider: stream.name, + id: featuredContent.id, + type: featuredContent.type + }); + } }, [featuredContent, navigation]); const refreshContinueWatching = useCallback(async () => { diff --git a/src/screens/StreamsScreen.tsx b/src/screens/StreamsScreen.tsx index ac5ef769e..5ac6a52ec 100644 --- a/src/screens/StreamsScreen.tsx +++ b/src/screens/StreamsScreen.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, memo, useState, useEffect } from 'react'; +import React, { useCallback, useMemo, memo, useState, useEffect, useRef, useLayoutEffect } from 'react'; import { View, Text, @@ -13,8 +13,9 @@ import { StatusBar, Alert, Dimensions, - Linking + Linking, } from 'react-native'; +import * as ScreenOrientation from 'expo-screen-orientation'; import { useRoute, useNavigation } from '@react-navigation/native'; import { RouteProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; @@ -241,10 +242,22 @@ export const StreamsScreen = () => { const { currentTheme } = useTheme(); const { colors } = currentTheme; + // Add ref to prevent excessive updates + const isMounted = useRef(true); + const loadStartTimeRef = useRef(0); + const hasDoneInitialLoadRef = useRef(false); + // Add timing logs const [loadStartTime, setLoadStartTime] = useState(0); const [providerLoadTimes, setProviderLoadTimes] = useState<{[key: string]: number}>({}); + // Prevent excessive re-renders by using this guard + const guardedSetState = useCallback((setter: () => void) => { + if (isMounted.current) { + setter(); + } + }, []); + const { metadata, episodes, @@ -285,54 +298,64 @@ export const StreamsScreen = () => { } }>({}); - // Monitor streams loading start and completion + // Monitor streams loading start and completion - FIXED to prevent loops useEffect(() => { + // Skip processing if component is unmounting + if (!isMounted.current) return; + const now = Date.now(); // Define all providers you expect to load. This could be dynamic. const expectedProviders = ['stremio', 'hdrezka']; + // Prevent infinite rerendering by using refs if (loadingStreams || loadingEpisodeStreams) { // --- Stream Loading has STARTED or is IN PROGRESS --- - logger.log("⏱️ Stream loading started or in progress..."); - - // Set load start time only if this is the beginning of a new loading cycle - if (loadStartTime === 0) { + // Only log once when loading starts + if (loadStartTimeRef.current === 0) { + logger.log("⏱️ Stream loading started or in progress..."); + // Update ref directly to avoid render cycle + loadStartTimeRef.current = now; + // Also update state for components that need it setLoadStartTime(now); } - setProviderLoadTimes({}); // Reset individual provider load times tracker + // Only update these once per loading cycle + if (!hasDoneInitialLoadRef.current) { + hasDoneInitialLoadRef.current = true; + + // Use the guarded setState to prevent issues after unmount + guardedSetState(() => setProviderLoadTimes({})); - // Update provider status to loading for all expected providers - setProviderStatus(prevStatus => { - const newStatus = { ...prevStatus }; - expectedProviders.forEach(providerId => { - // If not already marked as loading, or if it's a fresh cycle, set to loading - if (!newStatus[providerId] || !newStatus[providerId].loading || loadStartTime === 0) { - newStatus[providerId] = { - loading: true, - success: false, - error: false, - message: 'Loading...', - timeStarted: (newStatus[providerId]?.loading && newStatus[providerId]?.timeStarted) ? newStatus[providerId].timeStarted : now, - timeCompleted: 0, - }; - } - }); - return newStatus; - }); + // Update provider status to loading for all expected providers + guardedSetState(() => setProviderStatus(prevStatus => { + const newStatus = { ...prevStatus }; + expectedProviders.forEach(providerId => { + // If not already marked as loading, or if it's a fresh cycle, set to loading + if (!newStatus[providerId] || !newStatus[providerId].loading) { + newStatus[providerId] = { + loading: true, + success: false, + error: false, + message: 'Loading...', + timeStarted: (newStatus[providerId]?.loading && newStatus[providerId]?.timeStarted) ? newStatus[providerId].timeStarted : now, + timeCompleted: 0, + }; + } + }); + return newStatus; + })); - // Update simple loading flag for all expected providers - setLoadingProviders(prevLoading => { - const newLoading = { ...prevLoading }; - expectedProviders.forEach(providerId => { - newLoading[providerId] = true; - }); - return newLoading; - }); - - } else if (loadStartTime > 0) { + // Update simple loading flag for all expected providers + guardedSetState(() => setLoadingProviders(prevLoading => { + const newLoading = { ...prevLoading }; + expectedProviders.forEach(providerId => { + newLoading[providerId] = true; + }); + return newLoading; + })); + } + } else if (loadStartTimeRef.current > 0) { // --- Stream Loading has FINISHED --- - // (loadStartTime > 0 implies a loading cycle was active and has now completed) logger.log("🏁 Stream loading finished. Processing results."); const currentStreamsData = type === 'series' ? episodeStreams : groupedStreams; @@ -344,56 +367,53 @@ export const StreamsScreen = () => { logger.log(`📊 Providers with streams: ${providersWithStreams.join(', ')}`); - // Update simple loading flag: all expected providers are no longer loading - setLoadingProviders(prevLoading => { - const newLoading = { ...prevLoading }; - expectedProviders.forEach(providerId => { - newLoading[providerId] = false; - }); - return newLoading; - }); + // Reset refs for next load cycle + loadStartTimeRef.current = 0; + hasDoneInitialLoadRef.current = false; + + // Update states only if component is still mounted + if (isMounted.current) { + // Update simple loading flag: all expected providers are no longer loading + guardedSetState(() => setLoadingProviders(prevLoading => { + const newLoading = { ...prevLoading }; + expectedProviders.forEach(providerId => { + newLoading[providerId] = false; + }); + return newLoading; + })); - // Update detailed provider status based on results - setProviderStatus(prevStatus => { - const newStatus = { ...prevStatus }; - expectedProviders.forEach(providerId => { - if (newStatus[providerId]) { // Ensure the provider entry exists - const providerHasStreams = currentStreamsData[providerId] && - currentStreamsData[providerId].streams && - currentStreamsData[providerId].streams.length > 0; - - newStatus[providerId] = { - ...newStatus[providerId], // Preserve timeStarted - loading: false, - success: providerHasStreams, - // Mark error if it was loading and now no streams, and wasn't already successful - error: !providerHasStreams && newStatus[providerId].loading && !newStatus[providerId].success, - message: providerHasStreams ? 'Loaded successfully' : (newStatus[providerId].error ? 'Error or no streams' : 'No streams found'), - timeCompleted: now, - }; - } else { - // Fallback if somehow not initialized (should be caught by loading phase) - newStatus[providerId] = { - loading: false, - success: false, - error: true, - message: 'Provider status error (not initialized)', - timeStarted: 0, - timeCompleted: now, - }; - } - }); - return newStatus; - }); + // Update detailed provider status based on results + guardedSetState(() => setProviderStatus(prevStatus => { + const newStatus = { ...prevStatus }; + expectedProviders.forEach(providerId => { + if (newStatus[providerId]) { // Ensure the provider entry exists + const providerHasStreams = currentStreamsData[providerId] && + currentStreamsData[providerId].streams && + currentStreamsData[providerId].streams.length > 0; + + newStatus[providerId] = { + ...newStatus[providerId], // Preserve timeStarted + loading: false, + success: providerHasStreams, + // Mark error if it was loading and now no streams, and wasn't already successful + error: !providerHasStreams && newStatus[providerId].loading && !newStatus[providerId].success, + message: providerHasStreams ? 'Loaded successfully' : (newStatus[providerId].error ? 'Error or no streams' : 'No streams found'), + timeCompleted: now, + }; + } + }); + return newStatus; + })); - // Update the set of available providers based on what actually loaded streams - const providersWithStreamsSet = new Set(providersWithStreams); - setAvailableProviders(providersWithStreamsSet); + // Update the set of available providers based on what actually loaded streams + const providersWithStreamsSet = new Set(providersWithStreams); + guardedSetState(() => setAvailableProviders(providersWithStreamsSet)); - // Reset loadStartTime to signify the end of this loading cycle - setLoadStartTime(0); + // Reset loadStartTime to signify the end of this loading cycle + guardedSetState(() => setLoadStartTime(0)); + } } - }, [loadingStreams, loadingEpisodeStreams, groupedStreams, episodeStreams, type /* loadStartTime is intentionally omitted from deps here */]); + }, [loadingStreams, loadingEpisodeStreams, groupedStreams, episodeStreams, type, guardedSetState]); // Add useEffect to update availableProviders whenever streams change useEffect(() => { @@ -487,20 +507,44 @@ export const StreamsScreen = () => { ); }, [selectedEpisode, groupedEpisodes, id]); - const navigateToPlayer = useCallback((stream: Stream) => { - navigation.navigate('Player', { - uri: stream.url, - title: metadata?.name || '', - episodeTitle: type === 'series' ? currentEpisode?.name : undefined, - season: type === 'series' ? currentEpisode?.season_number : undefined, - episode: type === 'series' ? currentEpisode?.episode_number : undefined, - quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, - year: metadata?.year, - streamProvider: stream.name, - id, - type, - episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined - }); + const navigateToPlayer = useCallback(async (stream: Stream) => { + try { + // Lock orientation to landscape before navigation to prevent glitches + await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); + + // Small delay to ensure orientation is set before navigation + await new Promise(resolve => setTimeout(resolve, 100)); + + navigation.navigate('Player', { + uri: stream.url, + title: metadata?.name || '', + episodeTitle: type === 'series' ? currentEpisode?.name : undefined, + season: type === 'series' ? currentEpisode?.season_number : undefined, + episode: type === 'series' ? currentEpisode?.episode_number : undefined, + quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, + year: metadata?.year, + streamProvider: stream.name, + id, + type, + episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined + }); + } catch (error) { + logger.error('[StreamsScreen] Error locking orientation before navigation:', error); + // Fallback: navigate anyway + navigation.navigate('Player', { + uri: stream.url, + title: metadata?.name || '', + episodeTitle: type === 'series' ? currentEpisode?.name : undefined, + season: type === 'series' ? currentEpisode?.season_number : undefined, + episode: type === 'series' ? currentEpisode?.episode_number : undefined, + quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, + year: metadata?.year, + streamProvider: stream.name, + id, + type, + episodeId: type === 'series' && selectedEpisode ? selectedEpisode : undefined + }); + } }, [metadata, type, currentEpisode, navigation, id, selectedEpisode]); // Update handleStreamPress @@ -834,6 +878,13 @@ export const StreamsScreen = () => { ), [styles.streamGroupTitle]); + // Cleanup on unmount + useEffect(() => { + return () => { + isMounted.current = false; + }; + }, []); + return ( { + // This function only runs once per call site, avoiding render loops + // eslint-disable-next-line react-hooks/rules-of-hooks + useEffect(() => { + if (DEBUG_MODE) { + if (data) { + logger.log(`[VideoPlayer] ${message}`, data); + } else { + logger.log(`[VideoPlayer] ${message}`); + } + } + }, []); // Empty dependency array means this only runs once per mount +}; + // Constants for resume preferences - add after type definitions const RESUME_PREF_KEY = '@video_resume_preference'; const RESUME_PREF = { @@ -129,7 +149,16 @@ const languageMap: {[key: string]: string} = { const formatLanguage = (code?: string): string => { if (!code) return 'Unknown'; const normalized = code.toLowerCase(); - return languageMap[normalized] || code.toUpperCase(); + const languageName = languageMap[normalized] || code.toUpperCase(); + + // Debug logs removed to prevent render loops + + // If the result is still the uppercased code, it means we couldn't find it in our map. + if (languageName === code.toUpperCase()) { + return `Unknown (${code})`; + } + + return languageName; }; // Add VLC specific interface for their event structure @@ -144,6 +173,20 @@ interface VlcMediaEvent { selectedTextTrack?: number; } +// Helper function to extract a display name from the track's name property +const getTrackDisplayName = (track: { name?: string, id: number }): string => { + if (!track || !track.name) return `Track ${track.id}`; + + // Try to extract language from name like "Some Info - [English]" + const languageMatch = track.name.match(/\[(.*?)\]/); + if (languageMatch && languageMatch[1]) { + return languageMatch[1]; + } + + // If no language in brackets, or if the name is simple, use the full name + return track.name; +}; + const VideoPlayer: React.FC = () => { const navigation = useNavigation(); const route = useRoute>(); @@ -163,19 +206,10 @@ const VideoPlayer: React.FC = () => { episodeId } = route.params; - // Log received props for debugging - logger.log("[VideoPlayer] Received props:", { - uri, - title, - season, - episode, - episodeTitle, - quality, - year, - streamProvider, - id, - type, - episodeId + // Use safer debug logging for props + safeDebugLog("Component mounted with props", { + uri, title, season, episode, episodeTitle, quality, year, + streamProvider, id, type, episodeId }); const [paused, setPaused] = useState(false); @@ -186,7 +220,7 @@ const VideoPlayer: React.FC = () => { const [audioTracks, setAudioTracks] = useState([]); const [selectedAudioTrack, setSelectedAudioTrack] = useState(null); const [textTracks, setTextTracks] = useState([]); - const [selectedTextTrack, setSelectedTextTrack] = useState({ type: 'disabled' }); + const [selectedTextTrack, setSelectedTextTrack] = useState(-1); // Use -1 for "disabled" const [resizeMode, setResizeMode] = useState('contain'); // State for resize mode const [buffered, setBuffered] = useState(0); // Add buffered state const vlcRef = useRef(null); @@ -212,6 +246,12 @@ const VideoPlayer: React.FC = () => { // Add animated value for controls opacity const fadeAnim = useRef(new Animated.Value(1)).current; + // Add opening animation states and values + const [isOpeningAnimationComplete, setIsOpeningAnimationComplete] = useState(false); + const openingFadeAnim = useRef(new Animated.Value(0)).current; + const openingScaleAnim = useRef(new Animated.Value(0.8)).current; + const backgroundFadeAnim = useRef(new Animated.Value(1)).current; + // Add VLC specific state and refs const [isBuffering, setIsBuffering] = useState(false); @@ -219,17 +259,35 @@ const VideoPlayer: React.FC = () => { const [vlcAudioTracks, setVlcAudioTracks] = useState>([]); const [vlcTextTracks, setVlcTextTracks] = useState>([]); + // Add a new state to track if the player is ready for seeking + const [isPlayerReady, setIsPlayerReady] = useState(false); + + // Animated value for smooth progress bar + const progressAnim = useRef(new Animated.Value(0)).current; + + // Add ref for progress bar container to measure its width + const progressBarRef = useRef(null); + + // Add state for progress bar touch tracking + const [isDragging, setIsDragging] = useState(false); + + // Add a ref for debouncing seek operations + const seekDebounceTimer = useRef(null); + const pendingSeekValue = useRef(null); + const lastSeekTime = useRef(0); + // Lock screen to landscape when component mounts useEffect(() => { - const lockToLandscape = async () => { - await ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE); + // Since orientation is now locked before navigation, we can start immediately + const initializePlayer = () => { + // Enable immersive mode + enableImmersiveMode(); + + // Start the opening animation immediately + startOpeningAnimation(); }; - // Lock to landscape - lockToLandscape(); - - // Enable immersive mode when component mounts - enableImmersiveMode(); + initializePlayer(); // Restore screen orientation and disable immersive mode when component unmounts return () => { @@ -241,24 +299,68 @@ const VideoPlayer: React.FC = () => { }; }, []); + // Opening animation sequence + const startOpeningAnimation = () => { + // Much shorter delay since rotation is already handled + setTimeout(() => { + // Start the main animation sequence + Animated.parallel([ + // Fade in the video player + Animated.timing(openingFadeAnim, { + toValue: 1, + duration: 600, // Reduced back to original duration + useNativeDriver: true, + }), + // Scale up from 80% to 100% + Animated.timing(openingScaleAnim, { + toValue: 1, + duration: 700, // Reduced back to original duration + useNativeDriver: true, + }), + // Fade out the black background overlay + Animated.timing(backgroundFadeAnim, { + toValue: 0, + duration: 800, // Reduced back to original duration + useNativeDriver: true, + }), + ]).start(() => { + // Animation is complete + setIsOpeningAnimationComplete(true); + + // Hide the background overlay completely after animation + setTimeout(() => { + backgroundFadeAnim.setValue(0); + }, 100); + }); + }, 150); // Much shorter delay since no rotation is needed + }; + // 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'}`); + if (DEBUG_MODE) { + 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 (DEBUG_MODE) { + 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 (DEBUG_MODE) { + logger.log(`[VideoPlayer] Progress percent: ${progressPercent.toFixed(2)}%`); + } if (progressPercent < 95) { - logger.log(`[VideoPlayer] Setting initial position to ${savedProgress.currentTime}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Setting initial position to ${savedProgress.currentTime}`); + } // Set resume position setResumePosition(savedProgress.currentTime); @@ -266,25 +368,29 @@ const VideoPlayer: React.FC = () => { 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`); + if (DEBUG_MODE) { + 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`); + if (DEBUG_MODE) { + 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 { + } else if (DEBUG_MODE) { logger.log(`[VideoPlayer] Progress >= 95%, starting from beginning`); } } - } else { + } else if (DEBUG_MODE) { logger.log(`[VideoPlayer] No saved progress found`); } } catch (error) { logger.error('[VideoPlayer] Error loading watch progress:', error); } - } else { + } else if (DEBUG_MODE) { logger.log(`[VideoPlayer] Missing id or type, can't load progress. id=${id}, type=${type}`); } }; @@ -335,11 +441,10 @@ const VideoPlayer: React.FC = () => { 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 { + } else if (DEBUG_MODE) { logger.log(`[VideoPlayer] Cannot save progress: id=${id}, type=${type}, currentTime=${currentTime}, duration=${duration}`); } }; @@ -360,94 +465,160 @@ const VideoPlayer: React.FC = () => { } }; - const onSliderValueChange = (value: number) => { - if (vlcRef.current) { - const newTime = Math.floor(value); - vlcRef.current.seek(newTime); - setCurrentTime(newTime); - progress.value = newTime; + // Replace the reset seek value effect + // useEffect(() => { + // if (seekValue !== undefined) { + // const timer = setTimeout(() => { + // if (isMounted.current) { + // setSeekValue(undefined); + // } + // }, 1000); // Longer timeout to ensure VLC processes the seek properly + + // return () => clearTimeout(timer); + // } + // }, [seekValue]); + + // Simplify the seekToTime function to use VLC's direct methods + const seekToTime = (timeInSeconds: number) => { + if (!isPlayerReady || duration <= 0 || !vlcRef.current) return; + + // Calculate normalized position (0-1) for VLC + const normalizedPosition = Math.max(0, Math.min(timeInSeconds / duration, 1)); + + try { + // Use VLC's direct setPosition method + if (typeof vlcRef.current.setPosition === 'function') { + vlcRef.current.setPosition(normalizedPosition); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Called setPosition with ${normalizedPosition} for time: ${timeInSeconds}s`); + } + } else if (typeof vlcRef.current.seek === 'function') { + // Fallback to seek method if available + vlcRef.current.seek(normalizedPosition); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Called seek with ${normalizedPosition} for time: ${timeInSeconds}s`); + } + } else { + logger.error('[VideoPlayer] No seek method available on VLC player'); + } + + // Update UI immediately for responsiveness + const progressPercent = timeInSeconds / duration; + progressAnim.setValue(progressPercent); + + } catch (error) { + logger.error('[VideoPlayer] Error during seek operation:', error); } }; - const togglePlayback = () => { - if (vlcRef.current) { - if (paused) { - vlcRef.current.resume(); - } else { - vlcRef.current.pause(); + // Simplify handleProgress to always update state + const handleProgress = (event: any) => { + const currentTimeInSeconds = event.currentTime / 1000; // VLC gives time in milliseconds + + // Always update state - let VLC manage the timing + if (Math.abs(currentTimeInSeconds - currentTime) > 0.5) { + safeSetState(() => setCurrentTime(currentTimeInSeconds)); + progress.value = currentTimeInSeconds; + + // Animate the progress bar smoothly + const progressPercent = duration > 0 ? currentTimeInSeconds / duration : 0; + Animated.timing(progressAnim, { + toValue: progressPercent, + duration: 250, + useNativeDriver: false, + }).start(); + + // Update buffered position + const bufferedTime = event.bufferTime / 1000 || currentTimeInSeconds; + safeSetState(() => setBuffered(bufferedTime)); + } + }; + + // Enhanced onLoad handler to mark player as ready + const onLoad = (data: any) => { + setDuration(data.duration / 1000); // VLC returns duration in milliseconds + max.value = data.duration / 1000; + + // Mark player as ready for seeking + setIsPlayerReady(true); + + // Get audio and subtitle tracks from onLoad data + const audioTracksFromLoad = data.audioTracks || []; + const textTracksFromLoad = data.textTracks || []; + setVlcAudioTracks(audioTracksFromLoad); + setVlcTextTracks(textTracksFromLoad); + + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Video loaded with duration: ${data.duration / 1000}`); + const methods = Object.keys(vlcRef.current || {}).filter( + key => typeof vlcRef.current[key] === 'function' + ); + logger.log('[VideoPlayer] Available VLC methods:', methods); + logger.log('[VideoPlayer] Available audio tracks:', audioTracksFromLoad); + logger.log('[VideoPlayer] Available subtitle tracks:', textTracksFromLoad); + } + + // Set default selected tracks + if (audioTracksFromLoad.length > 1) { // More than just "Disable" + const firstEnabledAudio = audioTracksFromLoad.find((t: any) => t.id !== -1); + if(firstEnabledAudio) { + setSelectedAudioTrack(firstEnabledAudio.id); + } + } else if (audioTracksFromLoad.length > 0) { + setSelectedAudioTrack(audioTracksFromLoad[0].id); + } + // Subtitles default to disabled (-1) + + // If we have an initial position to seek to, do it now + if (initialPosition !== null && !isInitialSeekComplete) { + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Will seek to saved position: ${initialPosition}`); } - setPaused(!paused); + + // Seek with a short delay to ensure video is ready + setTimeout(() => { + if (vlcRef.current && duration > 0 && isMounted.current) { + seekToTime(initialPosition); + setIsInitialSeekComplete(true); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Initial seek completed to position: ${initialPosition}s`); + } + } + }, 1000); } }; const skip = (seconds: number) => { if (vlcRef.current) { const newTime = Math.max(0, Math.min(currentTime + seconds, duration)); - vlcRef.current.seek(newTime); - setCurrentTime(newTime); - progress.value = newTime; - } - }; - - const onProgress = (data: { currentTime: number }) => { - setCurrentTime(data.currentTime); - progress.value = data.currentTime; - }; - - const onLoad = (data: any) => { - setDuration(data.duration / 1000); // VLC returns duration in milliseconds - max.value = data.duration / 1000; - - logger.log(`[VideoPlayer] Video loaded with duration: ${data.duration / 1000}`); - - // If we have an initial position to seek to, do it now - if (initialPosition !== null && !isInitialSeekComplete && vlcRef.current) { - logger.log(`[VideoPlayer] Will seek to saved position: ${initialPosition}`); - - // Seek immediately with a small delay - setTimeout(() => { - if (vlcRef.current) { - try { - vlcRef.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] vlcRef 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] vlcRef not available for seeking`); - } + seekToTime(newTime); + // Let seekToTime handle all state updates } }; const onAudioTracks = (data: { audioTracks: AudioTrack[] }) => { const tracks = data.audioTracks || []; setAudioTracks(tracks); - logger.log(`[VideoPlayer] Available audio tracks:`, tracks); + if (DEBUG_MODE) { + 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); + if (DEBUG_MODE) { + 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]}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Changing aspect ratio from ${resizeMode} to ${resizeModes[nextIndex]}`); + } setResizeMode(resizeModes[nextIndex]); }; @@ -495,14 +666,14 @@ const VideoPlayer: React.FC = () => { // Add debug logs for modal visibility useEffect(() => { - if (showAudioModal) { + if (showAudioModal && DEBUG_MODE) { logger.log("[VideoPlayer] Audio modal should be visible now"); logger.log("[VideoPlayer] Available audio tracks:", audioTracks); } }, [showAudioModal, audioTracks]); useEffect(() => { - if (showSubtitleModal) { + if (showSubtitleModal && DEBUG_MODE) { logger.log("[VideoPlayer] Subtitle modal should be visible now"); logger.log("[VideoPlayer] Available text tracks:", textTracks); } @@ -511,13 +682,15 @@ const VideoPlayer: React.FC = () => { // Attempt to seek once vlcRef is available useEffect(() => { if (initialPosition !== null && !isInitialSeekComplete && vlcRef.current) { - logger.log(`[VideoPlayer] vlcRef is now available, attempting to seek to: ${initialPosition}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] vlcRef is now available, attempting to seek to: ${initialPosition}`); + } try { - vlcRef.current.seek(initialPosition); - setCurrentTime(initialPosition); - progress.value = initialPosition; + seekToTime(initialPosition); setIsInitialSeekComplete(true); - logger.log(`[VideoPlayer] Successfully seeked to position: ${initialPosition}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Successfully seeked to position: ${initialPosition}`); + } } catch (error) { logger.error('[VideoPlayer] Error seeking to position on ref available:', error); } @@ -531,17 +704,23 @@ const VideoPlayer: React.FC = () => { const pref = await AsyncStorage.getItem(RESUME_PREF_KEY); if (pref) { setResumePreference(pref); - logger.log(`[VideoPlayer] Loaded resume preference: ${pref}`); + if (DEBUG_MODE) { + 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`); + if (DEBUG_MODE) { + 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`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Auto-starting from beginning based on saved preference`); + } } } } catch (error) { @@ -557,7 +736,9 @@ const VideoPlayer: React.FC = () => { try { await AsyncStorage.removeItem(RESUME_PREF_KEY); setResumePreference(null); - logger.log(`[VideoPlayer] Reset resume preference`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Reset resume preference`); + } } catch (error) { logger.error('[VideoPlayer] Error resetting resume preference:', error); } @@ -566,13 +747,17 @@ const VideoPlayer: React.FC = () => { // Handle resume from overlay - modified for VLC const handleResume = async () => { if (resumePosition !== null && vlcRef.current) { - logger.log(`[VideoPlayer] Resuming from ${resumePosition}`); + if (DEBUG_MODE) { + 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}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Saved resume preference: ${RESUME_PREF.ALWAYS_RESUME}`); + } } catch (error) { logger.error('[VideoPlayer] Error saving resume preference:', error); } @@ -586,7 +771,7 @@ const VideoPlayer: React.FC = () => { // Seek to position with VLC setTimeout(() => { if (vlcRef.current) { - vlcRef.current.seek(resumePosition); + seekToTime(resumePosition); } }, 500); } @@ -594,13 +779,17 @@ const VideoPlayer: React.FC = () => { // Handle start from beginning - modified for VLC const handleStartFromBeginning = async () => { - logger.log(`[VideoPlayer] Starting from beginning`); + if (DEBUG_MODE) { + 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}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Saved resume preference: ${RESUME_PREF.ALWAYS_START_OVER}`); + } } catch (error) { logger.error('[VideoPlayer] Error saving resume preference:', error); } @@ -612,7 +801,7 @@ const VideoPlayer: React.FC = () => { setInitialPosition(0); // Make sure we seek to beginning if (vlcRef.current) { - vlcRef.current.seek(0); + seekToTime(0); setCurrentTime(0); progress.value = 0; } @@ -631,29 +820,6 @@ const VideoPlayer: React.FC = () => { setShowControls(!showControls); }; - // Handle VLC progress updates - const handleProgress = (event: any) => { - const currentTimeInSeconds = event.currentTime / 1000; // VLC gives time in milliseconds - setCurrentTime(currentTimeInSeconds); - progress.value = currentTimeInSeconds; - - // Update buffered position - const bufferedTime = event.bufferTime / 1000 || currentTimeInSeconds; - setBuffered(bufferedTime); - - // Calculate buffer ahead (cannot be negative) - const bufferAhead = Math.max(0, bufferedTime - currentTimeInSeconds); - const bufferPercentage = ((bufferedTime / (duration || 1)) * 100); - - // Add detailed buffer logging - logger.log(`[VideoPlayer] Buffer Status: - Current Time: ${currentTimeInSeconds.toFixed(2)}s - Buffered: ${bufferedTime.toFixed(2)}s - Buffered Ahead: ${bufferAhead.toFixed(2)}s - Buffer Percentage: ${bufferPercentage.toFixed(1)}% - `); - }; - // Handle VLC errors const handleError = (error: any) => { logger.error('[VideoPlayer] Playback Error:', error); @@ -663,7 +829,9 @@ const VideoPlayer: React.FC = () => { // Handle VLC buffering const onBuffering = (event: any) => { setIsBuffering(event.isBuffering); - logger.log(`[VideoPlayer] Buffering: ${event.isBuffering}`); + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Buffering: ${event.isBuffering}`); + } }; // Handle VLC playback ended @@ -671,111 +839,14 @@ const VideoPlayer: React.FC = () => { // Your existing playback ended logic here }; - // Function to get audio tracks from VLC - const getAudioTracks = () => { - if (vlcRef.current) { - vlcRef.current.getAudioTracks().then((tracks: any) => { - setVlcAudioTracks(tracks || []); - logger.log("[VideoPlayer] Available VLC audio tracks:", tracks); - }).catch((error: any) => { - logger.error("[VideoPlayer] Failed to get audio tracks:", error); - }); - } - }; - // Function to select audio track in VLC const selectAudioTrack = (trackId: number) => { - if (vlcRef.current) { - vlcRef.current.setAudioTrack(trackId); - setSelectedAudioTrack(trackId); - } - }; - - // Function to get subtitle tracks from VLC - const getTextTracks = () => { - if (vlcRef.current) { - vlcRef.current.getTextTracks().then((tracks: any) => { - setVlcTextTracks(tracks || []); - logger.log("[VideoPlayer] Available VLC subtitle tracks:", tracks); - }).catch((error: any) => { - logger.error("[VideoPlayer] Failed to get subtitle tracks:", error); - }); - } + setSelectedAudioTrack(trackId); }; // Function to select subtitle track in VLC const selectTextTrack = (trackId: number) => { - if (vlcRef.current) { - vlcRef.current.setTextTrack(trackId); - // Update your state accordingly - setSelectedTextTrack({ type: 'index', value: trackId }); - } - }; - - // Add this useEffect to get audio and subtitle tracks after player is loaded - useEffect(() => { - if (duration > 0 && vlcRef.current) { - // Wait a bit for VLC to fully initialize and recognize tracks - setTimeout(() => { - getAudioTracks(); - getTextTracks(); - }, 2000); - } - }, [duration]); - - // Update audio modal to use VLC audio tracks - const renderAudioModal = () => { - if (!showAudioModal) return null; - - return ( - - - - Audio - setShowAudioModal(false)} - > - - - - - - - {vlcAudioTracks.length > 0 ? vlcAudioTracks.map(track => ( - { - selectAudioTrack(track.id); - setShowAudioModal(false); - }} - > - - - {formatLanguage(track.language) || track.name || `Track ${track.id}`} - - {(track.name && track.language) && ( - {track.name} - )} - - {selectedAudioTrack === track.id && ( - - - - )} - - )) : ( - - - No audio tracks available - - )} - - - - - ); + setSelectedTextTrack(trackId); }; // Update subtitle modal to use VLC subtitle tracks @@ -808,8 +879,7 @@ const VideoPlayer: React.FC = () => { Off - {(selectedTextTrack?.type === 'disabled' || - (selectedTextTrack?.type === 'index' && selectedTextTrack.value === -1)) && ( + {selectedTextTrack === -1 && ( @@ -828,14 +898,13 @@ const VideoPlayer: React.FC = () => { > - {formatLanguage(track.language) || track.name || `Subtitle ${track.id}`} + {getTrackDisplayName(track)} {(track.name && track.language) && ( {track.name} )} - {selectedTextTrack?.type === 'index' && - selectedTextTrack?.value === track.id && ( + {selectedTextTrack === track.id && ( @@ -881,7 +950,11 @@ const VideoPlayer: React.FC = () => { // VLC specific method to set playback speed const changePlaybackSpeed = (speed: number) => { if (vlcRef.current) { - vlcRef.current.setRate(speed); + if (typeof vlcRef.current.setRate === 'function') { + vlcRef.current.setRate(speed); + } else if (typeof vlcRef.current.setPlaybackRate === 'function') { + vlcRef.current.setPlaybackRate(speed); + } setPlaybackSpeed(speed); } }; @@ -890,237 +963,422 @@ const VideoPlayer: React.FC = () => { const setVolume = (volumeLevel: number) => { if (vlcRef.current) { // VLC volume is typically between 0-200 - vlcRef.current.setVolume(volumeLevel * 200); + if (typeof vlcRef.current.setVolume === 'function') { + vlcRef.current.setVolume(volumeLevel * 200); + } } }; + // Added back the togglePlayback function + const togglePlayback = () => { + if (vlcRef.current) { + if (paused) { + // Check if resume function exists + if (typeof vlcRef.current.resume === 'function') { + vlcRef.current.resume(); + } else if (typeof vlcRef.current.play === 'function') { + vlcRef.current.play(); + } else { + // Fallback - use setPaused method or property if available + vlcRef.current.setPaused && vlcRef.current.setPaused(false); + } + } else { + // Check if pause function exists + if (typeof vlcRef.current.pause === 'function') { + vlcRef.current.pause(); + } else { + // Fallback - use setPaused method or property if available + vlcRef.current.setPaused && vlcRef.current.setPaused(true); + } + } + setPaused(!paused); + } + }; + + // Re-add the renderAudioModal function + const renderAudioModal = () => { + if (!showAudioModal) return null; + + return ( + + + + Audio + setShowAudioModal(false)} + > + + + + + + + {vlcAudioTracks.length > 0 ? vlcAudioTracks.map(track => ( + { + selectAudioTrack(track.id); + setShowAudioModal(false); + }} + > + + + {getTrackDisplayName(track)} + + {(track.name && track.language) && ( + {track.name} + )} + + {selectedAudioTrack === track.id && ( + + + + )} + + )) : ( + + + No audio tracks available + + )} + + + + + ); + }; + + // Use a ref to track if we're mounted to prevent state updates after unmount + // This helps prevent potential memory leaks and strange behaviors with navigation + const isMounted = useRef(true); + + // Clean up when component unmounts + useEffect(() => { + return () => { + isMounted.current = false; + if (seekDebounceTimer.current) { + clearTimeout(seekDebounceTimer.current); + } + }; + }, []); + + // Wrap all setState calls with this check + const safeSetState = (setter: any) => { + if (isMounted.current) { + setter(); + } + }; + + // Enhanced progress bar touch handling with drag support + const handleProgressBarTouch = (event: any) => { + if (!duration || duration <= 0) return; + + const { locationX } = event.nativeEvent; + processProgressTouch(locationX); + }; + + const handleProgressBarDragStart = () => { + setIsDragging(true); + }; + + const handleProgressBarDragMove = (event: any) => { + if (!isDragging || !duration || duration <= 0) return; + + const { locationX } = event.nativeEvent; + processProgressTouch(locationX); + }; + + const handleProgressBarDragEnd = () => { + setIsDragging(false); + }; + + // Helper function to process touch position and seek + const processProgressTouch = (locationX: number) => { + progressBarRef.current?.measure((x, y, width, height, pageX, pageY) => { + // Calculate percentage of touch position relative to progress bar width + const percentage = Math.max(0, Math.min(locationX / width, 1)); + // Calculate time to seek to + const seekTime = percentage * duration; + + if (DEBUG_MODE) { + logger.log(`[VideoPlayer] Seeking to: ${seekTime}s (${percentage * 100}%)`); + } + + // Seek to the calculated time + seekToTime(seekTime); + }); + }; + return ( - - + + + Loading video... + + - {/* Slider Container with buffer indicator */} - - - {/* Buffered Progress */} - - - + + - - {formatTime(currentTime)} - {formatTime(duration)} - - - {/* Controls Overlay - Using Animated.View */} - - {/* Top Gradient & Header */} - - - {/* Title Section - Enhanced with metadata */} - - {title} - {/* Show season and episode for series */} - {season && episode && ( - - S{season}E{episode} {episodeTitle && `• ${episodeTitle}`} - - )} - {/* Show year, quality, and provider */} - - {year && {year}} - {quality && {quality}} - {streamProvider && via {streamProvider}} - - - - - - - - - {/* Center Controls (Play/Pause, Skip) */} - - skip(-10)} style={styles.skipButton}> - - 10 - - - - - skip(10)} style={styles.skipButton}> - - 10 - - - - {/* Bottom Gradient */} - - - {/* Bottom Buttons Row */} - - {/* Speed Button */} - - - Speed ({playbackSpeed}x) - - - {/* Aspect Ratio Button - Added */} - - - - Aspect ({resizeMode}) - - - - {/* Audio Button - Updated language display */} - setShowAudioModal(true)} - disabled={audioTracks.length <= 1} - > - - - {audioTracks.length > 0 && selectedAudioTrack !== null - ? `Audio: ${formatLanguage(audioTracks.find(t => t.index === selectedAudioTrack)?.language)}` - : 'Audio: Default'} - - - - {/* Subtitle Button - Updated language display */} - setShowSubtitleModal(true)} - disabled={textTracks.length === 0} - > - - - {selectedTextTrack?.type === 'disabled' - ? 'Subtitles: Off' - : `Subtitles: ${formatLanguage(textTracks.find(t => t.index === selectedTextTrack?.value)?.language)}`} - - - - - - - - {/* Resume Overlay */} - {showResumeOverlay && resumePosition !== null && ( - - + - - - - - - Continue Watching - - {title} - {season && episode && ` • S${season}E${episode}`} - - - - 0 ? (resumePosition / duration) * 100 : 0}%` } - ]} - /> - - - {formatTime(resumePosition)} {duration > 0 ? `/ ${formatTime(duration)}` : ''} - - - - - - {/* Remember choice checkbox */} - setRememberChoice(!rememberChoice)} - activeOpacity={0.7} + - - - {rememberChoice && } - - Remember my choice + + {/* Buffered Progress */} + + {/* Animated Progress */} + - - {resumePreference && ( - - Reset - - )} + + + {formatTime(currentTime)} + {formatTime(duration)} + + - - - - Start Over - - - - Resume + {/* Controls Overlay - Using Animated.View */} + + {/* Top Gradient & Header */} + + + {/* Title Section - Enhanced with metadata */} + + {title} + {/* Show season and episode for series */} + {season && episode && ( + + S{season}E{episode} {episodeTitle && `• ${episodeTitle}`} + + )} + {/* Show year, quality, and provider */} + + {year && {year}} + {quality && {quality}} + {streamProvider && via {streamProvider}} + + + + - - )} - + + {/* Center Controls (Play/Pause, Skip) */} + + skip(-10)} style={styles.skipButton}> + + 10 + + + + + skip(10)} style={styles.skipButton}> + + 10 + + + + {/* Bottom Gradient */} + + + {/* Bottom Buttons Row */} + + {/* Speed Button */} + + + Speed ({playbackSpeed}x) + + + {/* Aspect Ratio Button - Added */} + + + + Aspect ({resizeMode}) + + + + {/* Audio Button - Updated to use vlcAudioTracks */} + setShowAudioModal(true)} + disabled={vlcAudioTracks.length <= 1} + > + + + {`Audio: ${getTrackDisplayName(vlcAudioTracks.find(t => t.id === selectedAudioTrack) || {id: -1, name: 'Default'})}`} + + + + {/* Subtitle Button - Updated to use vlcTextTracks */} + setShowSubtitleModal(true)} + disabled={vlcTextTracks.length === 0} + > + + + {(selectedTextTrack === -1) + ? 'Subtitles' + : `Subtitles: ${getTrackDisplayName(vlcTextTracks.find(t => t.id === selectedTextTrack) || {id: -1, name: 'On'})}`} + + + + + + + + {/* Resume Overlay */} + {showResumeOverlay && resumePosition !== null && ( + + + + + + + + Continue Watching + + {title} + {season && episode && ` • S${season}E${episode}`} + + + + 0 ? (resumePosition / duration) * 100 : 0}%` } + ]} + /> + + + {formatTime(resumePosition)} {duration > 0 ? `/ ${formatTime(duration)}` : ''} + + + + + + {/* Remember choice checkbox */} + setRememberChoice(!rememberChoice)} + activeOpacity={0.7} + > + + + {rememberChoice && } + + Remember my choice + + + {resumePreference && ( + + Reset + + )} + + + + + + Start Over + + + + Resume + + + + + )} + + {/* Use the new modal rendering functions */} {renderAudioModal()} @@ -1242,16 +1500,18 @@ const styles = StyleSheet.create({ paddingHorizontal: 20, zIndex: 1000, }, - sliderBackground: { - position: 'absolute', - left: 0, - right: 0, - height: 3, + progressTouchArea: { + height: 30, // Increase touch target height for easier interaction + justifyContent: 'center', + width: '100%', + }, + progressBarContainer: { + height: 4, backgroundColor: 'rgba(255, 255, 255, 0.2)', - borderRadius: 1.5, + borderRadius: 2, overflow: 'hidden', - marginHorizontal: 20, - top: 13.5, // Center with the slider thumb + marginHorizontal: 4, + position: 'relative', }, bufferProgress: { position: 'absolute', @@ -1260,17 +1520,20 @@ const styles = StyleSheet.create({ bottom: 0, backgroundColor: 'rgba(255, 255, 255, 0.4)', }, - slider: { - width: '100%', - height: 30, - zIndex: 1, + progressBarFill: { + position: 'absolute', + left: 0, + top: 0, + bottom: 0, + backgroundColor: '#E50914', + height: '100%', }, timeDisplay: { flexDirection: 'row', justifyContent: 'space-between', width: '100%', paddingHorizontal: 4, - marginTop: -4, // Reduced space between slider and time + marginTop: 4, // Increased space between progress bar and time marginBottom: 8, // Added space between time and buttons }, duration: { @@ -1565,6 +1828,33 @@ const styles = StyleSheet.create({ fontSize: 12, fontWeight: 'bold', }, + openingOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0,0,0,0.85)', + justifyContent: 'center', + alignItems: 'center', + zIndex: 2000, + }, + openingContent: { + padding: 20, + backgroundColor: 'rgba(0,0,0,0.85)', + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + }, + openingText: { + color: 'white', + fontSize: 18, + fontWeight: 'bold', + marginTop: 20, + }, + videoPlayerContainer: { + flex: 1, + }, }); export default VideoPlayer; \ No newline at end of file