diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index be64877f..894536b3 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -1195,6 +1195,7 @@ const AndroidVideoPlayer: React.FC = () => { handleProgressBarDragEnd={handleProgressBarDragEnd} buffered={buffered} formatTime={formatTime} + seekToTime={seekToTime} /> { const [currentSubtitle, setCurrentSubtitle] = useState(''); const [subtitleSize, setSubtitleSize] = useState(DEFAULT_SUBTITLE_SIZE); const [useCustomSubtitles, setUseCustomSubtitles] = useState(false); + const [subtitleBackground, setSubtitleBackground] = useState(true); // Add missing state const [isLoadingSubtitles, setIsLoadingSubtitles] = useState(false); const [availableSubtitles, setAvailableSubtitles] = useState([]); const [showSubtitleLanguageModal, setShowSubtitleLanguageModal] = useState(false); @@ -903,14 +904,20 @@ const VideoPlayer: React.FC = () => { }, []); const increaseSubtitleSize = () => { - const newSize = Math.min(subtitleSize + 2, 32); + const newSize = Math.min(subtitleSize + 2, 40); + setSubtitleSize(newSize); saveSubtitleSize(newSize); }; const decreaseSubtitleSize = () => { - const newSize = Math.max(subtitleSize - 2, 8); + const newSize = Math.max(subtitleSize - 2, 12); + setSubtitleSize(newSize); saveSubtitleSize(newSize); }; + + const toggleSubtitleBackground = () => { + setSubtitleBackground(prev => !prev); + }; useEffect(() => { if (pendingSeek && isPlayerReady && isVideoLoaded && duration > 0) { @@ -1204,6 +1211,7 @@ const VideoPlayer: React.FC = () => { handleProgressBarDragEnd={handleProgressBarDragEnd} buffered={buffered} formatTime={formatTime} + seekToTime={seekToTime} /> { currentSubtitle={currentSubtitle} subtitleSize={subtitleSize} zoomScale={zoomScale} + subtitleBackground={subtitleBackground} /> { selectedTextTrack={selectedTextTrack} useCustomSubtitles={useCustomSubtitles} subtitleSize={subtitleSize} + subtitleBackground={subtitleBackground} fetchAvailableSubtitles={fetchAvailableSubtitles} loadWyzieSubtitle={loadWyzieSubtitle} selectTextTrack={selectTextTrack} increaseSubtitleSize={increaseSubtitleSize} decreaseSubtitleSize={decreaseSubtitleSize} + toggleSubtitleBackground={toggleSubtitleBackground} /> void; buffered: number; formatTime: (seconds: number) => string; + seekToTime?: (time: number) => void; } +const { width: screenWidth } = Dimensions.get('window'); + export const PlayerControls: React.FC = ({ showControls, fadeAnim, @@ -75,63 +79,61 @@ export const PlayerControls: React.FC = ({ handleProgressBarDragEnd, buffered, formatTime, + seekToTime, }) => { + // State for tracking preview time during dragging + const [previewTime, setPreviewTime] = useState(null); + const [isDragging, setIsDragging] = useState(false); + + // Calculate slider width based on screen width minus padding + const sliderWidth = screenWidth - 40; // 20px padding on each side + + const handleSeek = (time: number) => { + if (seekToTime) { + seekToTime(time); + } + }; + + const handleSeekPreview = (time: number) => { + setPreviewTime(time); + }; + + const handleSeekStart = () => { + setIsDragging(true); + handleProgressBarDragStart(); + }; + + const handleSeekEnd = (time: number) => { + setIsDragging(false); + setPreviewTime(null); + handleProgressBarDragEnd(); + handleSeek(time); + }; + + // Determine which time to display (preview time while dragging, otherwise current time) + const displayTime = isDragging && previewTime !== null ? previewTime : currentTime; + return ( - {/* Progress bar with enhanced touch handling */} + {/* Progress bar with Skia slider */} - - - - {/* Buffered Progress */} - - {/* Animated Progress */} - - - - {/* Progress Thumb - Moved outside the progressBarContainer */} - - - + - {formatTime(currentTime)} + + {formatTime(displayTime)} + {formatTime(duration)} diff --git a/src/components/player/controls/SkiaProgressSlider.tsx b/src/components/player/controls/SkiaProgressSlider.tsx new file mode 100644 index 00000000..49df3a52 --- /dev/null +++ b/src/components/player/controls/SkiaProgressSlider.tsx @@ -0,0 +1,226 @@ +import React, { useMemo, useEffect } from 'react'; +import { View, Dimensions } from 'react-native'; +import { + Canvas, + RoundedRect, + Circle, + Group, + Shadow, +} from '@shopify/react-native-skia'; +import { + Gesture, + GestureDetector, + GestureHandlerRootView, +} from 'react-native-gesture-handler'; +import Animated, { + useSharedValue, + useAnimatedStyle, + withSpring, + runOnJS, + interpolate, +} from 'react-native-reanimated'; + +interface SkiaProgressSliderProps { + currentTime: number; + duration: number; + buffered: number; + onSeek: (time: number) => void; + onSeekStart?: () => void; + onSeekEnd?: (time: number) => void; + onSeekPreview?: (time: number) => void; // New callback for preview time + width: number; + height?: number; + thumbSize?: number; + progressColor?: string; + backgroundColor?: string; + bufferedColor?: string; +} + +export const SkiaProgressSlider: React.FC = ({ + currentTime, + duration, + buffered, + onSeek, + onSeekStart, + onSeekEnd, + onSeekPreview, + width, + height = 4, + thumbSize = 16, + progressColor = '#E50914', + backgroundColor = 'rgba(255, 255, 255, 0.2)', + bufferedColor = 'rgba(255, 255, 255, 0.4)', +}) => { + const progress = useSharedValue(0); + const isDragging = useSharedValue(false); + const thumbScale = useSharedValue(1); + const progressWidth = useSharedValue(0); + const previewTimeValue = useSharedValue(0); + + // Add padding to account for thumb size + const trackPadding = thumbSize / 2; + const trackWidth = width - (trackPadding * 2); + + // Update progress when currentTime changes + useEffect(() => { + if (!isDragging.value && duration > 0) { + const newProgress = (currentTime / duration); + progress.value = newProgress; + progressWidth.value = newProgress * trackWidth; + } + }, [currentTime, duration, trackWidth]); + + // Calculate buffered width + const bufferedWidth = useMemo(() => { + return duration > 0 ? (buffered / duration) * trackWidth : 0; + }, [buffered, duration, trackWidth]); + + // Handle seeking with proper coordinate mapping + const seekToPosition = (gestureX: number, isPreview: boolean = false) => { + 'worklet'; + // Map gesture coordinates to track coordinates + const trackX = gestureX - trackPadding; + const clampedX = Math.max(0, Math.min(trackX, trackWidth)); + + const newProgress = clampedX / trackWidth; + progress.value = newProgress; + progressWidth.value = clampedX; + + const seekTime = newProgress * duration; + previewTimeValue.value = seekTime; + + if (isPreview && onSeekPreview) { + runOnJS(onSeekPreview)(seekTime); + } else if (!isPreview) { + runOnJS(onSeek)(seekTime); + } + }; + + // Pan gesture for dragging + const panGesture = Gesture.Pan() + .onBegin((e) => { + 'worklet'; + isDragging.value = true; + thumbScale.value = withSpring(1.2, { damping: 15, stiffness: 400 }); + // Process the initial touch position immediately + seekToPosition(e.x, true); + if (onSeekStart) { + runOnJS(onSeekStart)(); + } + }) + .onUpdate((e) => { + 'worklet'; + seekToPosition(e.x, true); // Use preview mode during dragging + }) + .onFinalize((e) => { + 'worklet'; + isDragging.value = false; + thumbScale.value = withSpring(1, { damping: 15, stiffness: 400 }); + + // Use the exact same preview time for the final seek to ensure consistency + if (onSeekEnd) { + runOnJS(onSeekEnd)(previewTimeValue.value); + } + + // Final seek when drag ends - use the same calculation as preview + runOnJS(onSeek)(previewTimeValue.value); + }); + + // Tap gesture for seeking + const tapGesture = Gesture.Tap() + .onEnd((e) => { + 'worklet'; + seekToPosition(e.x, false); // Direct seek on tap + }); + + const composedGesture = Gesture.Simultaneous(tapGesture, panGesture); + + // Animated styles for thumb + const animatedThumbStyle = useAnimatedStyle(() => { + const thumbX = progress.value * trackWidth + trackPadding; + + return { + transform: [ + { translateX: thumbX - thumbSize / 2 }, + { scale: thumbScale.value } + ], + }; + }); + + // Animated style for progress width + const animatedProgressStyle = useAnimatedStyle(() => { + return { + width: progressWidth.value, + }; + }); + + return ( + + + + + + {/* Background track */} + + + {/* Buffered progress */} + {bufferedWidth > 0 && ( + + )} + + + + {/* Current progress - using regular view for animation */} + + + {/* Animated thumb */} + + + + + ); +}; \ No newline at end of file