UNPKG

@8man/react-native-media-console

Version:
521 lines (502 loc) 16 kB
import { View, Text, Pressable, Dimensions } from 'react-native'; import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import Animated, { useAnimatedStyle, withTiming, withSequence, useSharedValue, withDelay, runOnJS } from 'react-native-reanimated'; import Icon from '@expo/vector-icons/MaterialIcons'; import * as Brightness from 'expo-brightness'; import { VolumeManager } from 'react-native-volume-manager'; import { GestureDetector, Gesture, GestureHandlerRootView } from 'react-native-gesture-handler'; const SWIPE_RANGE = 370; const Ripple = /*#__PURE__*/React.memo(({ visible, isLeft, totalTime, showControls }) => { const screenDimensions = useMemo(() => Dimensions.get('window'), []); const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = screenDimensions; const scale = useSharedValue(0); const opacity = useSharedValue(0); React.useEffect(() => { if (visible) { scale.value = withSequence(withTiming(1.5, { duration: 400 }), withDelay(400, withTiming(0, { duration: 400 }))); opacity.value = withSequence(withTiming(0.4, { duration: 400 }), withDelay(400, withTiming(0, { duration: 400 }))); } }, [visible, scale, opacity]); const rippleStyle = useAnimatedStyle(() => ({ opacity: opacity.value, //@ts-ignore transform: [{ scale: scale.value }] }), []); const containerStyle = useMemo(() => ({ position: 'absolute', top: showControls ? -70 : -45, left: isLeft ? '-10%' : undefined, right: isLeft ? undefined : '-10%', width: SCREEN_WIDTH / 2.5, height: SCREEN_HEIGHT, zIndex: 999 }), [showControls, isLeft, SCREEN_WIDTH, SCREEN_HEIGHT]); const innerStyle = useMemo(() => ({ position: 'absolute', width: '100%', height: '100%', backgroundColor: 'rgba(0,0,0,0.9)', justifyContent: 'center', alignItems: 'center', borderRadius: SCREEN_HEIGHT / 2 }), [SCREEN_HEIGHT]); const textStyle = useMemo(() => ({ color: 'white', marginTop: 8, fontSize: 12 }), []); return visible ? /*#__PURE__*/React.createElement(View, { style: containerStyle }, /*#__PURE__*/React.createElement(Animated.View, { style: [innerStyle, rippleStyle] }, /*#__PURE__*/React.createElement(Icon, { name: isLeft ? 'fast-rewind' : 'fast-forward', size: 28, color: "white" }), !isNaN(totalTime) && totalTime > 0 && /*#__PURE__*/React.createElement(Text, { style: textStyle }, Math.floor(totalTime), "s"))) : null; }); const Gestures = ({ forward, rewind, togglePlayPause, toggleControls, doubleTapTime, tapActionTimeout, tapAnywhereToPause, rewindTime = 10, showControls, disableGesture, setPlayback }) => { const [rippleVisible, setRippleVisible] = useState(false); const [isLeftRipple, setIsLeftRipple] = useState(false); const [totalSkipTime, setTotalSkipTime] = useState(0); const [displayVolume, setDisplayVolume] = useState(0); const [displayBrightness, setDisplayBrightness] = useState(0); const [isVolumeVisible, setIsVolumeVisible] = useState(false); const [isBrightnessVisible, setIsBrightnessVisible] = useState(false); const [toastMessage, setToastMessage] = useState(null); // Memoize screen dimensions const screenDimensions = useMemo(() => Dimensions.get('window'), []); const { width: SCREEN_WIDTH } = screenDimensions; // Refs const initialTapPosition = useRef({ x: 0, y: 0 }); const isDoubleTapRef = useRef(false); const currentSideRef = useRef(null); const tapCountRef = useRef(0); const skipTimeoutRef = useRef(null); const lastTapTimeRef = useRef(0); const originalSettings = useRef({ volume: 0, brightness: 0 }); // Shared values const volumeValue = useSharedValue(0); const brightnessValue = useSharedValue(0); const startVolume = useSharedValue(0); const startBrightness = useSharedValue(0); const toastOpacity = useSharedValue(0); // Toast styles (inline, no Tailwind) const toastAnimatedStyle = useAnimatedStyle(() => ({ opacity: toastOpacity.value, transform: [{ translateY: (1 - toastOpacity.value) * -4 // subtle lift-in }] }), []); const toastContainerStyle = useMemo(() => ({ position: 'absolute', width: '100%', top: 48, // ~ top-12 justifyContent: 'center', alignItems: 'center', paddingHorizontal: 8, // px-2 zIndex: 1200 }), []); const toastTextStyle = useMemo(() => ({ color: 'white', // text-white backgroundColor: 'rgba(0,0,0,0.5)', // bg-black/50 padding: 8, // p-2 borderRadius: 9999, // rounded-full fontSize: 16 // text-base }), []); const show2xToast = useCallback(() => { setToastMessage('2× speed'); toastOpacity.value = withTiming(1, { duration: 150 }); }, [toastOpacity]); const hideToast = useCallback(() => { toastOpacity.value = withTiming(0, { duration: 150 }, finished => { if (finished) { runOnJS(setToastMessage)(null); } }); }, [toastOpacity]); const resetState = useCallback(() => { isDoubleTapRef.current = false; currentSideRef.current = null; tapCountRef.current = 0; lastTapTimeRef.current = 0; setTotalSkipTime(0); setRippleVisible(false); if (skipTimeoutRef.current) { clearTimeout(skipTimeoutRef.current); skipTimeoutRef.current = null; } }, []); const handleSkip = useCallback(async () => { try { const count = Number(tapCountRef.current) - 1; const baseTime = Number(rewindTime); const skipTime = baseTime * count; if (!isNaN(skipTime) && skipTime > 0) { if (currentSideRef.current === 'left') { rewind(skipTime); } else if (currentSideRef.current === 'right') { forward(skipTime); } } } catch (error) { console.error('Error while skipping:', error); } finally { resetState(); } }, [rewindTime, rewind, forward, resetState]); const handleTap = useCallback((e, side) => { const now = Date.now(); const touchX = e.nativeEvent.locationX; const touchY = e.nativeEvent.locationY; if (now - lastTapTimeRef.current > 500) { resetState(); } if (!isDoubleTapRef.current) { isDoubleTapRef.current = true; initialTapPosition.current = { x: touchX, y: touchY }; currentSideRef.current = side; tapCountRef.current = 1; lastTapTimeRef.current = now; tapActionTimeout.current = setTimeout(() => { if (tapAnywhereToPause) { togglePlayPause(); } else { toggleControls(); } resetState(); }, doubleTapTime); } else { if (tapActionTimeout.current) { clearTimeout(tapActionTimeout.current); tapActionTimeout.current = null; } if (currentSideRef.current === side) { tapCountRef.current += 1; lastTapTimeRef.current = now; const count = Number(tapCountRef.current) - 1; const baseTime = Number(rewindTime); const newSkipTime = baseTime * count; setTotalSkipTime(newSkipTime); setRippleVisible(true); setIsLeftRipple(side === 'left'); if (skipTimeoutRef.current) { clearTimeout(skipTimeoutRef.current); } skipTimeoutRef.current = setTimeout(handleSkip, 500); } else { resetState(); isDoubleTapRef.current = true; initialTapPosition.current = { x: touchX, y: touchY }; currentSideRef.current = side; tapCountRef.current = 1; lastTapTimeRef.current = now; tapActionTimeout.current = setTimeout(() => { resetState(); }, doubleTapTime); } } }, [resetState, tapAnywhereToPause, togglePlayPause, toggleControls, doubleTapTime, rewindTime, handleSkip]); const updateSystemVolume = useCallback(newVolume => { const clampedVolume = Math.max(0, Math.min(1, newVolume)); VolumeManager.setVolume(clampedVolume); setDisplayVolume(clampedVolume); }, []); const updateSystemBrightness = useCallback(newBrightness => { const clampedBrightness = Math.max(0, Math.min(1, newBrightness)); Brightness.setBrightnessAsync(clampedBrightness); setDisplayBrightness(clampedBrightness); }, []); const panGesture = useMemo(() => Gesture.Pan().minDistance(10) // Minimum distance before gesture starts .onStart(event => { 'worklet'; const isLeftSide = event.x < SCREEN_WIDTH / 2; if (isLeftSide) { startBrightness.value = brightnessValue.value; runOnJS(setIsBrightnessVisible)(true); } else { startVolume.value = volumeValue.value; runOnJS(setIsVolumeVisible)(true); } }).onUpdate(event => { 'worklet'; const isLeftSide = event.x < SCREEN_WIDTH / 2; const change = -event.translationY / SWIPE_RANGE; if (isLeftSide) { // Brightness control const newBrightness = Math.max(0, Math.min(1, startBrightness.value + change)); brightnessValue.value = newBrightness; runOnJS(updateSystemBrightness)(newBrightness); } else { // Volume control const newVolume = Math.max(0, Math.min(1, startVolume.value + change)); volumeValue.value = newVolume; runOnJS(updateSystemVolume)(newVolume); } }).onFinalize(() => { 'worklet'; runOnJS(setIsVolumeVisible)(false); runOnJS(setIsBrightnessVisible)(false); }), [SCREEN_WIDTH, updateSystemBrightness, updateSystemVolume]); const ControlOverlay = /*#__PURE__*/React.memo(({ value, isVisible, isVolume }) => { const containerStyle = useMemo(() => ({ position: 'absolute', top: '50%', left: !isVolume ? undefined : '15%', right: !isVolume ? '15%' : undefined, transform: [{ translateX: 0 }, { translateY: showControls ? -20 : 0 }], backgroundColor: 'rgba(0, 0, 0, 0.6)', borderRadius: 10, minWidth: 50, padding: 10, alignItems: 'center', zIndex: 1000 }), [isVolume, showControls]); const textStyle = useMemo(() => ({ color: 'white', marginTop: 5 }), []); const iconName = useMemo(() => { if (isVolume) { return value === 0 ? 'volume-mute' : value < 0.3 ? 'volume-down' : 'volume-up'; } return 'brightness-6'; }, [isVolume, value]); if (!isVisible) return null; return /*#__PURE__*/React.createElement(Animated.View, { style: containerStyle }, /*#__PURE__*/React.createElement(Icon, { name: iconName, size: 24, color: "white" }), /*#__PURE__*/React.createElement(Text, { style: textStyle }, Math.round(value * 100))); }); // Initialize and store original settings useEffect(() => { let mounted = true; const initializeSettings = async () => { try { const [currentVolume, currentBrightness] = await Promise.all([VolumeManager.getVolume(), Brightness.getBrightnessAsync()]); if (mounted) { // Store original values originalSettings.current = { volume: currentVolume.volume, brightness: currentBrightness }; // Set initial values volumeValue.value = currentVolume.volume; brightnessValue.value = currentBrightness; setDisplayVolume(currentVolume.volume); setDisplayBrightness(currentBrightness); console.log('Original settings stored:🔥', { volume: currentVolume, brightness: currentBrightness }); } } catch (error) { console.error('Error initializing settings:', error); } }; initializeSettings(); return () => { mounted = false; }; }, []); // Cleanup useEffect useEffect(() => { return () => { if (skipTimeoutRef.current) { clearTimeout(skipTimeoutRef.current); } if (tapActionTimeout.current) { clearTimeout(tapActionTimeout.current); } }; }, []); // Initialize and store original settings useEffect(() => { let mounted = true; const initializeSettings = async () => { try { const [currentVolume, currentBrightness] = await Promise.all([VolumeManager.getVolume(), Brightness.getBrightnessAsync()]); if (mounted) { // Store original values originalSettings.current = { volume: currentVolume.volume, brightness: currentBrightness }; // Set initial values volumeValue.value = currentVolume.volume; brightnessValue.value = currentBrightness; setDisplayVolume(currentVolume.volume); setDisplayBrightness(currentBrightness); } } catch (error) { console.error('Error initializing settings:', error); } }; initializeSettings(); // Cleanup function // return () => { // mounted = false; // const resetSettings = async () => { // try { // // console.log('Resetting to original settings:🔥', originalSettings.current); // await Promise.all([ // // SystemSetting.setVolume(originalSettings.current.volume), // SystemSetting.setAppBrightness(originalSettings.current.brightness), // ]); // // console.log('Settings reset successfully'); // } catch (error) { // console.error('Error resetting settings:', error); // } // }; // resetSettings(); // }; }, []); // Memoize container styles const containerStyle = useMemo(() => ({ width: '100%', height: '70%' }), []); const gestureContainerStyle = useMemo(() => ({ position: 'relative', width: '100%', height: '100%', flexDirection: 'row' }), []); const leftPressableStyle = useMemo(() => ({ flex: 1, top: 40, height: '100%', position: 'relative' }), []); const rightPressableStyle = useMemo(() => ({ top: 40, flex: 1, height: '100%', position: 'relative' }), []); // Memoized handler functions const handleLeftTap = useCallback(e => { handleTap(e, 'left'); }, [handleTap]); const handleRightTap = useCallback(e => { handleTap(e, 'right'); }, [handleTap]); if (disableGesture) { return null; } return /*#__PURE__*/React.createElement(GestureHandlerRootView, { style: containerStyle }, /*#__PURE__*/React.createElement(GestureDetector, { gesture: panGesture }, /*#__PURE__*/React.createElement(View, { style: gestureContainerStyle }, /*#__PURE__*/React.createElement(Pressable, { onPress: handleLeftTap, style: leftPressableStyle }, /*#__PURE__*/React.createElement(Ripple, { visible: rippleVisible && isLeftRipple, showControls: showControls, isLeft: true, totalTime: totalSkipTime })), /*#__PURE__*/React.createElement(Pressable, { onPress: handleRightTap, style: rightPressableStyle, onLongPress: () => { setPlayback(2); show2xToast(); }, onPressOut: () => { setPlayback(1); hideToast(); } }, /*#__PURE__*/React.createElement(Ripple, { visible: rippleVisible && !isLeftRipple, showControls: showControls, isLeft: false, totalTime: totalSkipTime })))), toastMessage ? /*#__PURE__*/React.createElement(Animated.View, { style: [toastContainerStyle, toastAnimatedStyle], pointerEvents: "none" }, /*#__PURE__*/React.createElement(Text, { style: toastTextStyle }, toastMessage)) : null, /*#__PURE__*/React.createElement(ControlOverlay, { value: displayVolume, isVisible: isVolumeVisible, isVolume: true }), /*#__PURE__*/React.createElement(ControlOverlay, { value: displayBrightness, isVisible: isBrightnessVisible, isVolume: false })); }; export default Gestures; //# sourceMappingURL=Gestures.js.map