From 94e165f0b02fc400b7cbef75f5bdc7eb1475e036 Mon Sep 17 00:00:00 2001 From: tapframe Date: Fri, 24 Oct 2025 22:35:42 +0530 Subject: [PATCH] improved player volume/brightness gesture sensitivity --- src/components/metadata/HeroSection.tsx | 46 +---- src/components/player/AndroidVideoPlayer.tsx | 178 +++---------------- src/components/player/KSPlayerCore.tsx | 151 +++------------- src/hooks/usePlayerGestureControls.ts | 171 ++++++++++++++++++ 4 files changed, 233 insertions(+), 313 deletions(-) create mode 100644 src/hooks/usePlayerGestureControls.ts diff --git a/src/components/metadata/HeroSection.tsx b/src/components/metadata/HeroSection.tsx index defb394d..dd799587 100644 --- a/src/components/metadata/HeroSection.tsx +++ b/src/components/metadata/HeroSection.tsx @@ -1106,9 +1106,6 @@ const HeroSection: React.FC = memo(({ // Guards to avoid repeated auto-starts const startedOnFocusRef = useRef(false); const startedOnReadyRef = useRef(false); - // Debounced pause/resume flag to avoid blocking scroll - const pendingPauseResumeSV = useSharedValue(0); // 0 = no action, 1 = pause, 2 = resume - const pauseResumeTimerRef = useRef(null); // Animation values for trailer unmute effects const actionButtonsOpacity = useSharedValue(1); @@ -1691,7 +1688,6 @@ const HeroSection: React.FC = memo(({ }, [isFocused, setTrailerPlaying]); // Ultra-optimized scroll-based pause/resume with cached calculations - // This worklet only sets flags - actual pause/resume happens asynchronously to avoid blocking scroll useDerivedValue(() => { 'worklet'; try { @@ -1704,49 +1700,21 @@ const HeroSection: React.FC = memo(({ const isPlaying = isPlayingSV.value === 1; const isPausedByScroll = pausedByScrollSV.value === 1; - // Set flags for pause/resume - don't execute immediately to keep scroll smooth + // Optimized pause/resume logic with minimal branching if (y > pauseThreshold && isPlaying && !isPausedByScroll) { - pendingPauseResumeSV.value = 1; // Request pause + pausedByScrollSV.value = 1; + runOnJS(setTrailerPlaying)(false); + isPlayingSV.value = 0; } else if (y < resumeThreshold && isPausedByScroll) { - pendingPauseResumeSV.value = 2; // Request resume + pausedByScrollSV.value = 0; + runOnJS(setTrailerPlaying)(true); + isPlayingSV.value = 1; } } catch (e) { // Silent error handling for performance } }); - // Debounced pause/resume effect - executes asynchronously to keep scroll smooth - useEffect(() => { - const checkPendingAction = () => { - const pendingAction = pendingPauseResumeSV.value; - - if (pendingAction === 1) { - // Execute pause - pausedByScrollSV.value = 1; - setTrailerPlaying(false); - isPlayingSV.value = 0; - pendingPauseResumeSV.value = 0; // Clear flag - } else if (pendingAction === 2) { - // Execute resume - pausedByScrollSV.value = 0; - setTrailerPlaying(true); - isPlayingSV.value = 1; - pendingPauseResumeSV.value = 0; // Clear flag - } - }; - - // Set up a recurring check with small delay to avoid blocking scroll - const intervalId = setInterval(checkPendingAction, 100); - - return () => { - clearInterval(intervalId); - if (pauseResumeTimerRef.current) { - clearTimeout(pauseResumeTimerRef.current); - pauseResumeTimerRef.current = null; - } - }; - }, [setTrailerPlaying, pendingPauseResumeSV, pausedByScrollSV, isPlayingSV]); - // Memory management and cleanup useEffect(() => { return () => { diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index 5cf5f82a..fc2b7ca3 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -17,6 +17,7 @@ import { useTraktAutosync } from '../../hooks/useTraktAutosync'; import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings'; import { useMetadata } from '../../hooks/useMetadata'; import { useSettings } from '../../hooks/useSettings'; +import { usePlayerGestureControls } from '../../hooks/usePlayerGestureControls'; import { DEFAULT_SUBTITLE_SIZE, @@ -278,8 +279,6 @@ const AndroidVideoPlayer: React.FC = () => { // Debounce resize operations to prevent rapid successive clicks const resizeTimeoutRef = useRef(null); - // Debounce gesture operations to prevent rapid-fire events - const gestureDebounceRef = useRef(null); @@ -376,10 +375,6 @@ const AndroidVideoPlayer: React.FC = () => { clearTimeout(resizeTimeoutRef.current); resizeTimeoutRef.current = null; } - if (gestureDebounceRef.current) { - clearTimeout(gestureDebounceRef.current); - gestureDebounceRef.current = null; - } }; }, []); @@ -576,16 +571,22 @@ const AndroidVideoPlayer: React.FC = () => { // Store Android system brightness state to restore on exit/unmount const originalSystemBrightnessRef = useRef(null); const originalSystemBrightnessModeRef = useRef(null); - const [showVolumeOverlay, setShowVolumeOverlay] = useState(false); - const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false); const [subtitleSettingsLoaded, setSubtitleSettingsLoaded] = useState(false); - const volumeOverlayOpacity = useRef(new Animated.Value(0)).current; - const brightnessOverlayOpacity = useRef(new Animated.Value(0)).current; - const volumeOverlayTimeout = useRef(null); - const brightnessOverlayTimeout = useRef(null); const lastVolumeChange = useRef(0); const lastBrightnessChange = useRef(0); + // Use reusable gesture controls hook + const gestureControls = usePlayerGestureControls({ + volume, + setVolume, + brightness, + setBrightness, + volumeRange: { min: 0, max: 1 }, + volumeSensitivity: 0.006, + brightnessSensitivity: 0.004, + debugMode: DEBUG_MODE, + }); + // iOS startup timing diagnostics const loadStartAtRef = useRef(null); const firstFrameAtRef = useRef(null); @@ -743,124 +744,6 @@ const AndroidVideoPlayer: React.FC = () => { return; }; - // Volume gesture handler (right side of screen) - optimized with debouncing - const onVolumeGestureEvent = async (event: PanGestureHandlerGestureEvent) => { - const { translationY, state } = event.nativeEvent; - const sensitivity = 0.002; // Lower sensitivity for gradual volume control on Android - - if (state === State.ACTIVE) { - // Debounce rapid gesture events - if (gestureDebounceRef.current) { - clearTimeout(gestureDebounceRef.current); - } - - gestureDebounceRef.current = setTimeout(() => { - const deltaY = -translationY; // Invert for natural feel (up = increase) - const volumeChange = deltaY * sensitivity; - const newVolume = Math.max(0, Math.min(1, volume + volumeChange)); - - if (Math.abs(newVolume - volume) > 0.01) { // Lower threshold for smoother Android volume control - setVolume(newVolume); - lastVolumeChange.current = Date.now(); - - if (DEBUG_MODE) { - logger.log(`[AndroidVideoPlayer] Volume set to: ${newVolume}`); - } - - // Show overlay with smoother animation - if (!showVolumeOverlay) { - setShowVolumeOverlay(true); - Animated.spring(volumeOverlayOpacity, { - toValue: 1, - tension: 100, - friction: 8, - useNativeDriver: true, - }).start(); - } - - // Clear existing timeout - if (volumeOverlayTimeout.current) { - clearTimeout(volumeOverlayTimeout.current); - } - - // Hide overlay after 1.5 seconds (reduced from 2 seconds) - volumeOverlayTimeout.current = setTimeout(() => { - Animated.timing(volumeOverlayOpacity, { - toValue: 0, - duration: 250, - useNativeDriver: true, - }).start(() => { - setShowVolumeOverlay(false); - }); - }, 1500); - } - }, 16); // ~60fps debouncing - } - }; - - // Brightness gesture handler (left side of screen) - optimized with debouncing - const onBrightnessGestureEvent = async (event: PanGestureHandlerGestureEvent) => { - const { translationY, state } = event.nativeEvent; - const sensitivity = 0.001; // Lower sensitivity for finer brightness control - - if (state === State.ACTIVE) { - // Debounce rapid gesture events - if (gestureDebounceRef.current) { - clearTimeout(gestureDebounceRef.current); - } - - gestureDebounceRef.current = setTimeout(() => { - const deltaY = -translationY; // Invert for natural feel (up = increase) - const brightnessChange = deltaY * sensitivity; - const newBrightness = Math.max(0, Math.min(1, brightness + brightnessChange)); - - if (Math.abs(newBrightness - brightness) > 0.001) { // Much lower threshold for more responsive updates - setBrightness(newBrightness); - lastBrightnessChange.current = Date.now(); - - // Set device brightness using Expo Brightness - try { - Brightness.setBrightnessAsync(newBrightness).catch((error) => { - logger.warn('[AndroidVideoPlayer] Error setting device brightness:', error); - }); - if (DEBUG_MODE) { - logger.log(`[AndroidVideoPlayer] Device brightness set to: ${newBrightness}`); - } - } catch (error) { - logger.warn('[AndroidVideoPlayer] Error setting device brightness:', error); - } - - // Show overlay with smoother animation - if (!showBrightnessOverlay) { - setShowBrightnessOverlay(true); - Animated.spring(brightnessOverlayOpacity, { - toValue: 1, - tension: 100, - friction: 8, - useNativeDriver: true, - }).start(); - } - - // Clear existing timeout - if (brightnessOverlayTimeout.current) { - clearTimeout(brightnessOverlayTimeout.current); - } - - // Hide overlay after 1.5 seconds (reduced from 2 seconds) - brightnessOverlayTimeout.current = setTimeout(() => { - Animated.timing(brightnessOverlayOpacity, { - toValue: 0, - duration: 250, - useNativeDriver: true, - }).start(() => { - setShowBrightnessOverlay(false); - }); - }, 1500); - } - }, 16); // ~60fps debouncing - } - }; - // Long press gesture handlers for speed boost const onLongPressActivated = useCallback(() => { if (!holdToSpeedEnabled) return; @@ -2902,14 +2785,6 @@ const AndroidVideoPlayer: React.FC = () => { clearTimeout(errorTimeoutRef.current); errorTimeoutRef.current = null; } - if (volumeOverlayTimeout.current) { - clearTimeout(volumeOverlayTimeout.current); - volumeOverlayTimeout.current = null; - } - if (brightnessOverlayTimeout.current) { - clearTimeout(brightnessOverlayTimeout.current); - brightnessOverlayTimeout.current = null; - } if (controlsTimeout.current) { clearTimeout(controlsTimeout.current); controlsTimeout.current = null; @@ -2918,14 +2793,13 @@ const AndroidVideoPlayer: React.FC = () => { clearTimeout(pauseOverlayTimerRef.current); pauseOverlayTimerRef.current = null; } - if (gestureDebounceRef.current) { - clearTimeout(gestureDebounceRef.current); - gestureDebounceRef.current = null; - } if (progressSaveInterval) { clearInterval(progressSaveInterval); setProgressSaveInterval(null); } + + // Cleanup gesture controls + gestureControls.cleanup(); // Best-effort restore of Android system brightness state on unmount if (Platform.OS === 'android') { try { @@ -3323,9 +3197,9 @@ const AndroidVideoPlayer: React.FC = () => { simultaneousHandlers={[]} > { simultaneousHandlers={[]} > { /> {/* Volume Overlay */} - {showVolumeOverlay && ( + {gestureControls.showVolumeOverlay && ( @@ -4071,13 +3945,13 @@ const AndroidVideoPlayer: React.FC = () => { )} {/* Brightness Overlay */} - {showBrightnessOverlay && ( + {gestureControls.showBrightnessOverlay && ( diff --git a/src/components/player/KSPlayerCore.tsx b/src/components/player/KSPlayerCore.tsx index 540a0bd9..0a8be547 100644 --- a/src/components/player/KSPlayerCore.tsx +++ b/src/components/player/KSPlayerCore.tsx @@ -18,6 +18,7 @@ import { useTraktAutosync } from '../../hooks/useTraktAutosync'; import { useTraktAutosyncSettings } from '../../hooks/useTraktAutosyncSettings'; import { useMetadata } from '../../hooks/useMetadata'; import { useSettings } from '../../hooks/useSettings'; +import { usePlayerGestureControls } from '../../hooks/usePlayerGestureControls'; import { DEFAULT_SUBTITLE_SIZE, @@ -125,6 +126,7 @@ const KSPlayerCore: React.FC = () => { const ksPlayerRef = useRef(null); const [showAudioModal, setShowAudioModal] = useState(false); const [showSubtitleModal, setShowSubtitleModal] = useState(false); + const [showSpeedModal, setShowSpeedModal] = useState(false); const [initialPosition, setInitialPosition] = useState(null); const [progressSaveInterval, setProgressSaveInterval] = useState(null); const [isInitialSeekComplete, setIsInitialSeekComplete] = useState(false); @@ -297,15 +299,19 @@ const KSPlayerCore: React.FC = () => { // Volume and brightness controls const [volume, setVolume] = useState(100); // KSPlayer uses 0-100 range const [brightness, setBrightness] = useState(1.0); - const [showVolumeOverlay, setShowVolumeOverlay] = useState(false); - const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false); const [subtitleSettingsLoaded, setSubtitleSettingsLoaded] = useState(false); - const volumeOverlayOpacity = useRef(new Animated.Value(0)).current; - const brightnessOverlayOpacity = useRef(new Animated.Value(0)).current; - const volumeOverlayTimeout = useRef(null); - const brightnessOverlayTimeout = useRef(null); - const lastVolumeChange = useRef(0); - const lastBrightnessChange = useRef(0); + + // Use reusable gesture controls hook + const gestureControls = usePlayerGestureControls({ + volume, + setVolume, + brightness, + setBrightness, + volumeRange: { min: 0, max: 100 }, // KSPlayer uses 0-100 + volumeSensitivity: 0.006, + brightnessSensitivity: 0.004, + debugMode: DEBUG_MODE, + }); // Get metadata to access logo (only if we have a valid id) const shouldLoadMetadata = Boolean(id && type); @@ -462,104 +468,6 @@ const KSPlayerCore: React.FC = () => { } }; - // Volume gesture handler (right side of screen) - const onVolumeGestureEvent = async (event: PanGestureHandlerGestureEvent) => { - const { translationY, state } = event.nativeEvent; - const sensitivity = 0.050; // Higher sensitivity for volume (more responsive than brightness) - - if (state === State.ACTIVE) { - const deltaY = -translationY; // Invert for natural feel (up = increase) - const volumeChange = deltaY * sensitivity; - const newVolume = Math.max(0, Math.min(100, volume + volumeChange)); - - if (Math.abs(newVolume - volume) > 0.05) { // Even lower threshold for volume responsiveness - setVolume(newVolume); - lastVolumeChange.current = Date.now(); - - // Show overlay with smoother animation - if (!showVolumeOverlay) { - setShowVolumeOverlay(true); - Animated.spring(volumeOverlayOpacity, { - toValue: 1, - tension: 100, - friction: 8, - useNativeDriver: true, - }).start(); - } - - // Clear existing timeout - if (volumeOverlayTimeout.current) { - clearTimeout(volumeOverlayTimeout.current); - } - - // Hide overlay after 1.5 seconds - volumeOverlayTimeout.current = setTimeout(() => { - Animated.timing(volumeOverlayOpacity, { - toValue: 0, - duration: 250, - useNativeDriver: true, - }).start(() => { - setShowVolumeOverlay(false); - }); - }, 1500); - } - } - }; - - // Brightness gesture handler (left side of screen) - const onBrightnessGestureEvent = async (event: PanGestureHandlerGestureEvent) => { - const { translationY, state } = event.nativeEvent; - const sensitivity = 0.001; // Lower sensitivity for finer brightness control - - if (state === State.ACTIVE) { - const deltaY = -translationY; // Invert for natural feel (up = increase) - const brightnessChange = deltaY * sensitivity; - const newBrightness = Math.max(0, Math.min(1, brightness + brightnessChange)); - - if (Math.abs(newBrightness - brightness) > 0.001) { // Much lower threshold for more responsive updates - setBrightness(newBrightness); - lastBrightnessChange.current = Date.now(); - - // Set device brightness using Expo Brightness - try { - await Brightness.setBrightnessAsync(newBrightness); - if (DEBUG_MODE) { - logger.log(`[VideoPlayer] Device brightness set to: ${newBrightness}`); - } - } catch (error) { - logger.warn('[VideoPlayer] Error setting device brightness:', error); - } - - // Show overlay with smoother animation - if (!showBrightnessOverlay) { - setShowBrightnessOverlay(true); - Animated.spring(brightnessOverlayOpacity, { - toValue: 1, - tension: 100, - friction: 8, - useNativeDriver: true, - }).start(); - } - - // Clear existing timeout - if (brightnessOverlayTimeout.current) { - clearTimeout(brightnessOverlayTimeout.current); - } - - // Hide overlay after 1.5 seconds (reduced from 2 seconds) - brightnessOverlayTimeout.current = setTimeout(() => { - Animated.timing(brightnessOverlayOpacity, { - toValue: 0, - duration: 250, - useNativeDriver: true, - }).start(() => { - setShowBrightnessOverlay(false); - }); - }, 1500); - } - } - }; - useEffect(() => { if (videoAspectRatio && effectiveDimensions.width > 0 && effectiveDimensions.height > 0) { const styles = calculateVideoStyles( @@ -2211,12 +2119,10 @@ const KSPlayerCore: React.FC = () => { if (errorTimeoutRef.current) { clearTimeout(errorTimeoutRef.current); } - if (volumeOverlayTimeout.current) { - clearTimeout(volumeOverlayTimeout.current); - } - if (brightnessOverlayTimeout.current) { - clearTimeout(brightnessOverlayTimeout.current); - } + + // Cleanup gesture controls + gestureControls.cleanup(); + if (startupRetryTimerRef.current) { clearTimeout(startupRetryTimerRef.current); startupRetryTimerRef.current = null; @@ -2619,9 +2525,9 @@ const KSPlayerCore: React.FC = () => { > {/* Combined gesture handler for left side - brightness + tap */} { {/* Combined gesture handler for right side - volume + tap */} { currentResizeMode={resizeMode} setShowAudioModal={setShowAudioModal} setShowSubtitleModal={setShowSubtitleModal} + setShowSpeedModal={setShowSpeedModal} isSubtitleModalOpen={showSubtitleModal} setShowSourcesModal={setShowSourcesModal} onSliderValueChange={handleSliderValueChange} @@ -3169,13 +3076,13 @@ const KSPlayerCore: React.FC = () => { /> {/* Volume Overlay */} - {showVolumeOverlay && ( + {gestureControls.showVolumeOverlay && ( @@ -3266,13 +3173,13 @@ const KSPlayerCore: React.FC = () => { )} {/* Brightness Overlay */} - {showBrightnessOverlay && ( + {gestureControls.showBrightnessOverlay && ( diff --git a/src/hooks/usePlayerGestureControls.ts b/src/hooks/usePlayerGestureControls.ts new file mode 100644 index 00000000..2030ea2e --- /dev/null +++ b/src/hooks/usePlayerGestureControls.ts @@ -0,0 +1,171 @@ +import { useRef, useState } from 'react'; +import { Animated, Platform } from 'react-native'; +import { PanGestureHandlerGestureEvent, State } from 'react-native-gesture-handler'; +import * as Brightness from 'expo-brightness'; + +interface GestureControlConfig { + volume: number; + setVolume: (value: number) => void; + brightness: number; + setBrightness: (value: number) => void; + volumeRange?: { min: number; max: number }; // Default: { min: 0, max: 1 } + volumeSensitivity?: number; // Default: 0.006 (iOS), 0.0084 (Android with 1.4x multiplier) + brightnessSensitivity?: number; // Default: 0.004 (iOS), 0.0056 (Android with 1.4x multiplier) + overlayTimeout?: number; // Default: 1500ms + debugMode?: boolean; +} + +export const usePlayerGestureControls = (config: GestureControlConfig) => { + // State for overlays + const [showVolumeOverlay, setShowVolumeOverlay] = useState(false); + const [showBrightnessOverlay, setShowBrightnessOverlay] = useState(false); + + // Animated values + const volumeGestureTranslateY = useRef(new Animated.Value(0)).current; + const brightnessGestureTranslateY = useRef(new Animated.Value(0)).current; + const volumeOverlayOpacity = useRef(new Animated.Value(0)).current; + const brightnessOverlayOpacity = useRef(new Animated.Value(0)).current; + + // Tracking refs + const lastVolumeGestureY = useRef(0); + const lastBrightnessGestureY = useRef(0); + const volumeOverlayTimeout = useRef(null); + const brightnessOverlayTimeout = useRef(null); + + // Extract config with defaults and platform adjustments + const volumeRange = config.volumeRange || { min: 0, max: 1 }; + const baseVolumeSensitivity = config.volumeSensitivity || 0.006; + const baseBrightnessSensitivity = config.brightnessSensitivity || 0.004; + const overlayTimeout = config.overlayTimeout || 1500; + + // Platform-specific sensitivity adjustments + // Android needs higher sensitivity due to different touch handling + const platformMultiplier = Platform.OS === 'android' ? 1.6 : 1.0; + const volumeSensitivity = baseVolumeSensitivity * platformMultiplier; + const brightnessSensitivity = baseBrightnessSensitivity * platformMultiplier; + + // Volume gesture handler + const onVolumeGestureEvent = Animated.event( + [{ nativeEvent: { translationY: volumeGestureTranslateY } }], + { + useNativeDriver: false, + listener: (event: PanGestureHandlerGestureEvent) => { + const { translationY, state } = event.nativeEvent; + + if (state === State.ACTIVE) { + // Auto-initialize on first active frame + if (Math.abs(translationY) < 5 && Math.abs(lastVolumeGestureY.current - translationY) > 20) { + lastVolumeGestureY.current = translationY; + return; + } + + // Calculate delta from last position + const deltaY = -(translationY - lastVolumeGestureY.current); + lastVolumeGestureY.current = translationY; + + // Normalize sensitivity based on volume range + const rangeMultiplier = volumeRange.max - volumeRange.min; + const volumeChange = deltaY * volumeSensitivity * rangeMultiplier; + const newVolume = Math.max(volumeRange.min, Math.min(volumeRange.max, config.volume + volumeChange)); + + config.setVolume(newVolume); + + if (config.debugMode) { + console.log(`[GestureControls] Volume set to: ${newVolume} (Platform: ${Platform.OS}, Sensitivity: ${volumeSensitivity})`); + } + + // Show overlay + if (!showVolumeOverlay) { + setShowVolumeOverlay(true); + volumeOverlayOpacity.setValue(1); + } + + // Reset hide timer + if (volumeOverlayTimeout.current) { + clearTimeout(volumeOverlayTimeout.current); + } + volumeOverlayTimeout.current = setTimeout(() => { + Animated.timing(volumeOverlayOpacity, { + toValue: 0, + duration: 250, + useNativeDriver: true, + }).start(() => setShowVolumeOverlay(false)); + }, overlayTimeout); + } + } + } + ); + + // Brightness gesture handler + const onBrightnessGestureEvent = Animated.event( + [{ nativeEvent: { translationY: brightnessGestureTranslateY } }], + { + useNativeDriver: false, + listener: (event: PanGestureHandlerGestureEvent) => { + const { translationY, state } = event.nativeEvent; + + if (state === State.ACTIVE) { + // Auto-initialize + if (Math.abs(translationY) < 5 && Math.abs(lastBrightnessGestureY.current - translationY) > 20) { + lastBrightnessGestureY.current = translationY; + return; + } + + const deltaY = -(translationY - lastBrightnessGestureY.current); + lastBrightnessGestureY.current = translationY; + + const brightnessChange = deltaY * brightnessSensitivity; + const newBrightness = Math.max(0, Math.min(1, config.brightness + brightnessChange)); + + config.setBrightness(newBrightness); + Brightness.setBrightnessAsync(newBrightness).catch(() => {}); + + if (config.debugMode) { + console.log(`[GestureControls] Device brightness set to: ${newBrightness} (Platform: ${Platform.OS}, Sensitivity: ${brightnessSensitivity})`); + } + + if (!showBrightnessOverlay) { + setShowBrightnessOverlay(true); + brightnessOverlayOpacity.setValue(1); + } + + if (brightnessOverlayTimeout.current) { + clearTimeout(brightnessOverlayTimeout.current); + } + brightnessOverlayTimeout.current = setTimeout(() => { + Animated.timing(brightnessOverlayOpacity, { + toValue: 0, + duration: 250, + useNativeDriver: true, + }).start(() => setShowBrightnessOverlay(false)); + }, overlayTimeout); + } + } + } + ); + + // Cleanup function + const cleanup = () => { + if (volumeOverlayTimeout.current) { + clearTimeout(volumeOverlayTimeout.current); + } + if (brightnessOverlayTimeout.current) { + clearTimeout(brightnessOverlayTimeout.current); + } + }; + + return { + // Gesture handlers + onVolumeGestureEvent, + onBrightnessGestureEvent, + + // Overlay state + showVolumeOverlay, + showBrightnessOverlay, + volumeOverlayOpacity, + brightnessOverlayOpacity, + + // Cleanup + cleanup, + }; +};