From ed3aef88ff9ba327810f71dc5388396ca5c3400c Mon Sep 17 00:00:00 2001 From: tapframe Date: Fri, 24 Oct 2025 21:54:30 +0530 Subject: [PATCH] Added subtitlemodal --- src/components/player/AndroidVideoPlayer.tsx | 130 ++++++- .../player/controls/PlayerControls.tsx | 4 +- src/components/player/modals/SpeedModal.tsx | 330 ++++++++++++++++++ 3 files changed, 459 insertions(+), 5 deletions(-) create mode 100644 src/components/player/modals/SpeedModal.tsx diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index dc9f9262..5cf5f82a 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -30,10 +30,14 @@ import { RESUME_PREF, SUBTITLE_SIZE_KEY } from './utils/playerTypes'; + +// Speed settings storage key +const SPEED_SETTINGS_KEY = '@nuvio_speed_settings'; import { safeDebugLog, parseSRT, DEBUG_MODE, formatTime } from './utils/playerUtils'; import { styles } from './utils/playerStyles'; import { SubtitleModals } from './modals/SubtitleModals'; import { AudioTrackModal } from './modals/AudioTrackModal'; +import SpeedModal from './modals/SpeedModal'; // Removed ResumeOverlay usage when alwaysResume is enabled import PlayerControls from './controls/PlayerControls'; import CustomSubtitles from './subtitles/CustomSubtitles'; @@ -219,6 +223,54 @@ const AndroidVideoPlayer: React.FC = () => { // Speed boost state for hold-to-speed-up feature const [isSpeedBoosted, setIsSpeedBoosted] = useState(false); const [originalSpeed, setOriginalSpeed] = useState(1.0); + const [showSpeedActivatedOverlay, setShowSpeedActivatedOverlay] = useState(false); + const speedActivatedOverlayOpacity = useRef(new Animated.Value(0)).current; + + // Speed modal state + const [showSpeedModal, setShowSpeedModal] = useState(false); + const [holdToSpeedEnabled, setHoldToSpeedEnabled] = useState(true); + const [holdToSpeedValue, setHoldToSpeedValue] = useState(2.0); + + // Load speed settings from storage + const loadSpeedSettings = useCallback(async () => { + try { + const saved = await AsyncStorage.getItem(SPEED_SETTINGS_KEY); + if (saved) { + const settings = JSON.parse(saved); + if (typeof settings.holdToSpeedEnabled === 'boolean') { + setHoldToSpeedEnabled(settings.holdToSpeedEnabled); + } + if (typeof settings.holdToSpeedValue === 'number') { + setHoldToSpeedValue(settings.holdToSpeedValue); + } + } + } catch (error) { + logger.warn('[AndroidVideoPlayer] Error loading speed settings:', error); + } + }, []); + + // Save speed settings to storage + const saveSpeedSettings = useCallback(async () => { + try { + const settings = { + holdToSpeedEnabled, + holdToSpeedValue, + }; + await AsyncStorage.setItem(SPEED_SETTINGS_KEY, JSON.stringify(settings)); + } catch (error) { + logger.warn('[AndroidVideoPlayer] Error saving speed settings:', error); + } + }, [holdToSpeedEnabled, holdToSpeedValue]); + + // Load speed settings on mount + useEffect(() => { + loadSpeedSettings(); + }, [loadSpeedSettings]); + + // Save speed settings when they change + useEffect(() => { + saveSpeedSettings(); + }, [saveSpeedSettings]); // Debounce track updates to prevent excessive processing const trackUpdateTimeoutRef = useRef(null); @@ -811,14 +863,36 @@ const AndroidVideoPlayer: React.FC = () => { // Long press gesture handlers for speed boost const onLongPressActivated = useCallback(() => { - if (!isSpeedBoosted && playbackSpeed !== 2.0) { + if (!holdToSpeedEnabled) return; + + if (!isSpeedBoosted && playbackSpeed !== holdToSpeedValue) { setOriginalSpeed(playbackSpeed); - setPlaybackSpeed(2.0); + setPlaybackSpeed(holdToSpeedValue); setIsSpeedBoosted(true); - logger.log('[AndroidVideoPlayer] Speed boost activated: 2x'); + // Show "Activated" overlay + setShowSpeedActivatedOverlay(true); + Animated.spring(speedActivatedOverlayOpacity, { + toValue: 1, + tension: 100, + friction: 8, + useNativeDriver: true, + }).start(); + + // Auto-hide after 2 seconds + setTimeout(() => { + Animated.timing(speedActivatedOverlayOpacity, { + toValue: 0, + duration: 300, + useNativeDriver: true, + }).start(() => { + setShowSpeedActivatedOverlay(false); + }); + }, 2000); + + logger.log(`[AndroidVideoPlayer] Speed boost activated: ${holdToSpeedValue}x`); } - }, [isSpeedBoosted, playbackSpeed]); + }, [isSpeedBoosted, playbackSpeed, holdToSpeedEnabled, holdToSpeedValue, speedActivatedOverlayOpacity]); const onLongPressEnd = useCallback(() => { if (isSpeedBoosted) { @@ -3510,6 +3584,7 @@ const AndroidVideoPlayer: React.FC = () => { currentPlaybackSpeed={playbackSpeed} setShowAudioModal={setShowAudioModal} setShowSubtitleModal={setShowSubtitleModal} + setShowSpeedModal={setShowSpeedModal} isSubtitleModalOpen={showSubtitleModal} setShowSourcesModal={setShowSourcesModal} onSliderValueChange={handleSliderValueChange} @@ -4092,6 +4167,42 @@ const AndroidVideoPlayer: React.FC = () => { )} + {/* Speed Activated Overlay */} + {showSpeedActivatedOverlay && ( + + + + {holdToSpeedValue}x Speed Activated + + + + )} + {/* Resume overlay removed when AlwaysResume is enabled; overlay component omitted */} @@ -4151,6 +4262,17 @@ const AndroidVideoPlayer: React.FC = () => { setSubtitleOffsetSec={setSubtitleOffsetSec} /> + + void; setShowSubtitleModal: (show: boolean) => void; + setShowSpeedModal: (show: boolean) => void; isSubtitleModalOpen?: boolean; setShowSourcesModal?: (show: boolean) => void; // Slider-specific props @@ -77,6 +78,7 @@ export const PlayerControls: React.FC = ({ currentPlaybackSpeed, setShowAudioModal, setShowSubtitleModal, + setShowSpeedModal, isSubtitleModalOpen, setShowSourcesModal, onSliderValueChange, @@ -537,7 +539,7 @@ export const PlayerControls: React.FC = ({ {/* Playback Speed Button - Temporarily hidden on iOS */} {Platform.OS !== 'ios' && ( - + setShowSpeedModal(true)}> Speed {currentPlaybackSpeed}x diff --git a/src/components/player/modals/SpeedModal.tsx b/src/components/player/modals/SpeedModal.tsx new file mode 100644 index 00000000..11757f00 --- /dev/null +++ b/src/components/player/modals/SpeedModal.tsx @@ -0,0 +1,330 @@ +import React from 'react'; +import { View, Text, TouchableOpacity, Platform, useWindowDimensions, ScrollView } from 'react-native'; +import { MaterialIcons } from '@expo/vector-icons'; +import Animated, { + FadeIn, + FadeOut, + SlideInRight, + SlideOutRight, +} from 'react-native-reanimated'; +import { useSafeAreaInsets } from 'react-native-safe-area-context'; + +interface SpeedModalProps { + showSpeedModal: boolean; + setShowSpeedModal: (show: boolean) => void; + currentSpeed: number; + setPlaybackSpeed: (speed: number) => void; + holdToSpeedEnabled: boolean; + setHoldToSpeedEnabled: (enabled: boolean) => void; + holdToSpeedValue: number; + setHoldToSpeedValue: (speed: number) => void; +} + +export const SpeedModal: React.FC = ({ + showSpeedModal, + setShowSpeedModal, + currentSpeed, + setPlaybackSpeed, + holdToSpeedEnabled, + setHoldToSpeedEnabled, + holdToSpeedValue, + setHoldToSpeedValue, +}) => { + const insets = useSafeAreaInsets(); + const { width, height } = useWindowDimensions(); + const isIos = Platform.OS === 'ios'; + const isLandscape = width > height; + + // Responsive tuning - more aggressive compacting + const isCompact = width < 360 || height < 640; + const sectionPad = isCompact ? 6 : 8; + const chipPadH = isCompact ? 4 : 6; + const chipPadV = isCompact ? 3 : 4; + const menuWidth = Math.min( + width * (isIos ? (isLandscape ? 0.55 : 0.8) : 0.85), + isIos ? 380 : 360 + ); + + const speedPresets = [0.5, 1.0, 1.5, 2.0, 2.5, 3.0]; + const holdSpeedOptions = [1.5, 2.0, 2.5, 3.0]; + + const handleClose = () => { + setShowSpeedModal(false); + }; + + const handleSpeedSelect = (speed: number) => { + setPlaybackSpeed(speed); + }; + + const handleHoldSpeedSelect = (speed: number) => { + setHoldToSpeedValue(speed); + }; + + const renderSpeedModal = () => { + if (!showSpeedModal) return null; + + return ( + <> + {/* Backdrop */} + + + + + {/* Side Menu */} + + {/* Header */} + + + + Playback Speed + + + + + + + + + {/* Current Speed Display */} + + + + + Current: {currentSpeed}x + + + + + {/* Speed Presets */} + + + Speed Presets + + + + {speedPresets.map((speed) => { + const isSelected = currentSpeed === speed; + return ( + handleSpeedSelect(speed)} + style={{ + paddingHorizontal: chipPadH, + paddingVertical: chipPadV, + borderRadius: 12, + backgroundColor: isSelected ? 'rgba(59, 130, 246, 0.15)' : 'rgba(255, 255, 255, 0.05)', + borderWidth: 1, + borderColor: isSelected ? 'rgba(59, 130, 246, 0.3)' : 'rgba(255, 255, 255, 0.1)', + minWidth: 50, + alignItems: 'center', + }} + activeOpacity={0.7} + > + + {speed}x + + + ); + })} + + + + {/* Hold-to-Speed Settings - Android Only */} + {Platform.OS === 'android' && ( + + + + + + Hold-to-Speed + + + + {/* Enable Toggle */} + + Enable Hold Speed + setHoldToSpeedEnabled(!holdToSpeedEnabled)} + > + + + + + {/* Hold Speed Selector */} + {holdToSpeedEnabled && ( + + Hold Speed + + {holdSpeedOptions.map((speed) => { + const isSelected = holdToSpeedValue === speed; + return ( + handleHoldSpeedSelect(speed)} + style={{ + paddingHorizontal: chipPadH, + paddingVertical: chipPadV, + borderRadius: 10, + backgroundColor: isSelected ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255, 255, 255, 0.05)', + borderWidth: 1, + borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)', + minWidth: 45, + alignItems: 'center', + }} + activeOpacity={0.7} + > + + {speed}x + + + ); + })} + + + )} + + {/* Info Text */} + + + + + + Hold left/right sides + + + Hold and press the left or right side of the video player to temporarily boost playback speed. + + + + + + + )} + + + + ); + }; + + return ( + <> + {renderSpeedModal()} + + ); +}; + +export default SpeedModal;