diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index dd6ced40..d5646f2b 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -4,7 +4,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useNavigation, useRoute, RouteProp, useFocusEffect } from '@react-navigation/native'; import FastImage from '@d11/react-native-fast-image'; import { RootStackParamList, RootStackNavigationProp } from '../../navigation/AppNavigator'; -import { PinchGestureHandler, PanGestureHandler, TapGestureHandler, State, PinchGestureHandlerGestureEvent, PanGestureHandlerGestureEvent, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler'; +import { PinchGestureHandler, PanGestureHandler, TapGestureHandler, LongPressGestureHandler, State, PinchGestureHandlerGestureEvent, PanGestureHandlerGestureEvent, TapGestureHandlerGestureEvent, LongPressGestureHandlerGestureEvent } from 'react-native-gesture-handler'; import RNImmersiveMode from 'react-native-immersive-mode'; import * as ScreenOrientation from 'expo-screen-orientation'; import { storageService } from '../../services/storageService'; @@ -34,8 +34,12 @@ import { } from './utils/playerTypes'; import { safeDebugLog, parseSRT, DEBUG_MODE, formatTime } from './utils/playerUtils'; import { styles } from './utils/playerStyles'; + +// Speed settings storage key +const SPEED_SETTINGS_KEY = '@nuvio_speed_settings'; 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'; @@ -199,8 +203,15 @@ const KSPlayerCore: React.FC = () => { const [showSourcesModal, setShowSourcesModal] = useState(false); const [availableStreams, setAvailableStreams] = useState<{ [providerId: string]: { streams: any[]; addonName: string } }>(passedAvailableStreams || {}); // Playback speed controls required by PlayerControls - const speedOptions = [0.5, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0]; + const speedOptions = [0.5, 1.0, 1.25, 1.5, 2.0, 2.5]; const [playbackSpeed, setPlaybackSpeed] = useState(1.0); + // Hold-to-speed-up feature state + const [holdToSpeedEnabled, setHoldToSpeedEnabled] = useState(true); + const [holdToSpeedValue, setHoldToSpeedValue] = useState(2.0); + const [isSpeedBoosted, setIsSpeedBoosted] = useState(false); + const [originalSpeed, setOriginalSpeed] = useState(1.0); + const [showSpeedActivatedOverlay, setShowSpeedActivatedOverlay] = useState(false); + const speedActivatedOverlayOpacity = useRef(new Animated.Value(0)).current; const cyclePlaybackSpeed = useCallback(() => { const idx = speedOptions.indexOf(playbackSpeed); const nextIdx = (idx + 1) % speedOptions.length; @@ -270,6 +281,47 @@ const KSPlayerCore: React.FC = () => { debugMode: DEBUG_MODE, }); + // Load speed settings from storage + const loadSpeedSettings = useCallback(async () => { + try { + const saved = await mmkvStorage.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('[KSPlayerCore] Error loading speed settings:', error); + } + }, []); + + // Save speed settings to storage + const saveSpeedSettings = useCallback(async () => { + try { + const settings = { + holdToSpeedEnabled, + holdToSpeedValue, + }; + await mmkvStorage.setItem(SPEED_SETTINGS_KEY, JSON.stringify(settings)); + } catch (error) { + logger.warn('[KSPlayerCore] Error saving speed settings:', error); + } + }, [holdToSpeedEnabled, holdToSpeedValue]); + + // Load speed settings on mount + useEffect(() => { + loadSpeedSettings(); + }, [loadSpeedSettings]); + + // Save speed settings when they change + useEffect(() => { + saveSpeedSettings(); + }, [saveSpeedSettings]); + // Get metadata to access logo (only if we have a valid id) const shouldLoadMetadata = Boolean(id && type); const metadataResult = useMetadata({ @@ -434,6 +486,48 @@ const KSPlayerCore: React.FC = () => { } }; + // Long press gesture handlers for speed boost + const onLongPressActivated = useCallback(() => { + if (!holdToSpeedEnabled) return; + + if (!isSpeedBoosted && playbackSpeed !== holdToSpeedValue) { + setOriginalSpeed(playbackSpeed); + setPlaybackSpeed(holdToSpeedValue); + setIsSpeedBoosted(true); + + // 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(`[KSPlayerCore] Speed boost activated: ${holdToSpeedValue}x`); + } + }, [isSpeedBoosted, playbackSpeed, holdToSpeedEnabled, holdToSpeedValue, speedActivatedOverlayOpacity]); + + const onLongPressEnd = useCallback(() => { + if (isSpeedBoosted) { + setPlaybackSpeed(originalSpeed); + setIsSpeedBoosted(false); + + logger.log('[KSPlayerCore] Speed boost deactivated, restored to:', originalSpeed); + } + }, [isSpeedBoosted, originalSpeed]); + useEffect(() => { if (videoAspectRatio && effectiveDimensions.width > 0 && effectiveDimensions.height > 0) { const styles = calculateVideoStyles( @@ -2489,55 +2583,71 @@ const KSPlayerCore: React.FC = () => { } ]} > - {/* Combined gesture handler for left side - brightness + tap */} - - - - - + + + + + - {/* Combined gesture handler for right side - volume + tap */} - - - - - + + + + + {/* Center area tap handler - handles both show and hide */} { )} + {/* Speed Activated Overlay */} + {showSpeedActivatedOverlay && ( + + + + {holdToSpeedValue}x Speed Activated + + + + )} {/* Resume overlay removed when AlwaysResume is enabled; overlay component omitted */} @@ -3247,6 +3392,16 @@ const KSPlayerCore: React.FC = () => { selectedAudioTrack={selectedAudioTrack} selectAudioTrack={selectAudioTrack} /> + = ({ - {/* Playback Speed Button - Temporarily hidden on iOS */} - {Platform.OS !== 'ios' && ( - setShowSpeedModal(true)}> - - - Speed {currentPlaybackSpeed}x - - - )} + {/* Playback Speed Button */} + setShowSpeedModal(true)}> + + + Speed {currentPlaybackSpeed}x + + {/* Audio Button - Updated to use ksAudioTracks */} = ({ 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 speedPresets = [0.5, 1.0, 1.5, 2.0, 2.5]; + const holdSpeedOptions = [1.5, 2.0]; const handleClose = () => { setShowSpeedModal(false); @@ -215,9 +215,8 @@ export const SpeedModal: React.FC = ({ - {/* Hold-to-Speed Settings - Android Only */} - {Platform.OS === 'android' && ( - + {/* Hold-to-Speed Settings */} + @@ -313,7 +312,6 @@ export const SpeedModal: React.FC = ({ - )}