import React, { useEffect, useState, useRef } from 'react'; import { View, StyleSheet, TouchableOpacity, Text, TouchableWithoutFeedback, PanResponder, } from 'react-native'; import { useVideoPlayer, VideoView } from 'expo-video'; import { MaterialIcons } from '@expo/vector-icons'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; interface CustomVideoPlayerProps { uri: string; shouldPlay: boolean; onScrubStart?: () => void; onScrubEnd?: () => void; } export default function CustomVideoPlayer({ uri, shouldPlay, onScrubStart, onScrubEnd, }: CustomVideoPlayerProps) { const insets = useSafeAreaInsets(); const [isPlaying, setIsPlaying] = useState(shouldPlay); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [showControls, setShowControls] = useState(true); const [scrubberWidth, setScrubberWidth] = useState(0); const scrubberPageXRef = useRef(0); const controlsTimeoutRef = useRef | null>(null); const player = useVideoPlayer(uri, (p) => { p.loop = true; if (shouldPlay) p.play(); }); useEffect(() => { if (shouldPlay) { player.play(); setIsPlaying(true); } else { player.pause(); setIsPlaying(false); } }, [shouldPlay, player]); useEffect(() => { const interval = setInterval(() => { if (player) { setCurrentTime(player.currentTime); if (player.duration) setDuration(player.duration); } }, 250); return () => clearInterval(interval); }, [player]); const resetControlsTimeout = () => { setShowControls(true); if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current); controlsTimeoutRef.current = setTimeout(() => { setShowControls(false); }, 3000); }; useEffect(() => { if (isPlaying) { resetControlsTimeout(); } else { if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current); setShowControls(true); } return () => { if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current); }; }, [isPlaying]); const togglePlayPause = () => { if (isPlaying) { player.pause(); setIsPlaying(false); } else { player.play(); setIsPlaying(true); } }; const handleScreenTap = () => { if (showControls) { setShowControls(false); if (controlsTimeoutRef.current) clearTimeout(controlsTimeoutRef.current); } else { resetControlsTimeout(); } }; const seekFromAbsoluteX = (absX: number) => { if (duration <= 0 || scrubberWidth <= 0) return; const relX = absX - scrubberPageXRef.current; const pct = Math.max(0, Math.min(1, relX / scrubberWidth)); const seekTime = pct * duration; player.currentTime = seekTime; setCurrentTime(seekTime); }; const handleScrubberLayout = () => { scrubberViewRef.current?.measureInWindow((x, _y, w, _h) => { scrubberPageXRef.current = x; setScrubberWidth(w); }); }; const scrubberViewRef = useRef(null); /* ── Scrubber gesture ── Uses standard React Native PanResponder to forcefully capture touches and prevent the parent FlatList from scrolling horizontally. */ const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: () => true, onMoveShouldSetPanResponder: () => true, onPanResponderGrant: (e) => { onScrubStart?.(); seekFromAbsoluteX(e.nativeEvent.pageX); }, onPanResponderMove: (e) => { seekFromAbsoluteX(e.nativeEvent.pageX); }, onPanResponderRelease: () => { onScrubEnd?.(); resetControlsTimeout(); }, onPanResponderTerminate: () => { onScrubEnd?.(); resetControlsTimeout(); }, }) ).current; const formatTime = (secs: number) => { const m = Math.floor(secs / 60); const s = Math.floor(secs % 60); return `${m}:${s < 10 ? '0' : ''}${s}`; }; const progressPercent = duration > 0 ? (currentTime / duration) * 100 : 0; return ( {showControls && ( {formatTime(currentTime)} {/* Scrubber — Using PanResponder ensures FlatList stops scrolling */} {formatTime(duration)} )} ); } const styles = StyleSheet.create({ container: { flex: 1, width: '100%', height: '100%', backgroundColor: '#000', justifyContent: 'center', alignItems: 'center', }, video: { width: '100%', height: '100%', }, overlay: { ...StyleSheet.absoluteFillObject, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.3)', }, playPauseBtn: { width: 80, height: 80, borderRadius: 40, backgroundColor: 'rgba(0,0,0,0.6)', justifyContent: 'center', alignItems: 'center', }, bottomBar: { position: 'absolute', bottom: 0, left: 0, right: 0, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, }, timeText: { color: '#fff', fontSize: 13, fontWeight: '600', width: 44, textAlign: 'center', }, scrubberContainer: { flex: 1, height: 44, justifyContent: 'center', marginHorizontal: 8, }, scrubberTrack: { height: 4, backgroundColor: 'rgba(255,255,255,0.3)', borderRadius: 2, justifyContent: 'center', }, scrubberFill: { height: 4, backgroundColor: '#1a73e8', borderRadius: 2, position: 'absolute', left: 0, }, scrubberThumb: { width: 16, height: 16, borderRadius: 8, backgroundColor: '#1a73e8', position: 'absolute', marginLeft: -8, }, });