improved player volume/brightness gesture sensitivity

This commit is contained in:
tapframe 2025-10-24 22:35:42 +05:30
parent ed3aef88ff
commit 94e165f0b0
4 changed files with 233 additions and 313 deletions

View file

@ -1106,9 +1106,6 @@ const HeroSection: React.FC<HeroSectionProps> = 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<any>(null);
// Animation values for trailer unmute effects
const actionButtonsOpacity = useSharedValue(1);
@ -1691,7 +1688,6 @@ const HeroSection: React.FC<HeroSectionProps> = 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<HeroSectionProps> = 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 () => {

View file

@ -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<NodeJS.Timeout | null>(null);
// Debounce gesture operations to prevent rapid-fire events
const gestureDebounceRef = useRef<NodeJS.Timeout | null>(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<number | null>(null);
const originalSystemBrightnessModeRef = useRef<number | null>(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<NodeJS.Timeout | null>(null);
const brightnessOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
const lastVolumeChange = useRef<number>(0);
const lastBrightnessChange = useRef<number>(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<number | null>(null);
const firstFrameAtRef = useRef<number | null>(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={[]}
>
<PanGestureHandler
onGestureEvent={onBrightnessGestureEvent}
activeOffsetY={[-5, 5]}
failOffsetX={[-20, 20]}
onGestureEvent={gestureControls.onBrightnessGestureEvent}
activeOffsetY={[-10, 10]}
failOffsetX={[-30, 30]}
shouldCancelWhenOutside={false}
simultaneousHandlers={[]}
maxPointers={1}
@ -3356,9 +3230,9 @@ const AndroidVideoPlayer: React.FC = () => {
simultaneousHandlers={[]}
>
<PanGestureHandler
onGestureEvent={onVolumeGestureEvent}
activeOffsetY={[-5, 5]}
failOffsetX={[-20, 20]}
onGestureEvent={gestureControls.onVolumeGestureEvent}
activeOffsetY={[-10, 10]}
failOffsetX={[-30, 30]}
shouldCancelWhenOutside={false}
simultaneousHandlers={[]}
maxPointers={1}
@ -3974,13 +3848,13 @@ const AndroidVideoPlayer: React.FC = () => {
/>
{/* Volume Overlay */}
{showVolumeOverlay && (
{gestureControls.showVolumeOverlay && (
<Animated.View
style={{
position: 'absolute',
left: screenDimensions.width / 2 - 60,
top: screenDimensions.height / 2 - 60,
opacity: volumeOverlayOpacity,
opacity: gestureControls.volumeOverlayOpacity,
zIndex: 1000,
}}
>
@ -4071,13 +3945,13 @@ const AndroidVideoPlayer: React.FC = () => {
)}
{/* Brightness Overlay */}
{showBrightnessOverlay && (
{gestureControls.showBrightnessOverlay && (
<Animated.View
style={{
position: 'absolute',
left: screenDimensions.width / 2 - 60,
top: screenDimensions.height / 2 - 60,
opacity: brightnessOverlayOpacity,
opacity: gestureControls.brightnessOverlayOpacity,
zIndex: 1000,
}}
>

View file

@ -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<KSPlayerRef>(null);
const [showAudioModal, setShowAudioModal] = useState(false);
const [showSubtitleModal, setShowSubtitleModal] = useState(false);
const [showSpeedModal, setShowSpeedModal] = useState(false);
const [initialPosition, setInitialPosition] = useState<number | null>(null);
const [progressSaveInterval, setProgressSaveInterval] = useState<NodeJS.Timeout | null>(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<NodeJS.Timeout | null>(null);
const brightnessOverlayTimeout = useRef<NodeJS.Timeout | null>(null);
const lastVolumeChange = useRef<number>(0);
const lastBrightnessChange = useRef<number>(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 */}
<PanGestureHandler
onGestureEvent={onBrightnessGestureEvent}
activeOffsetY={[-5, 5]}
failOffsetX={[-20, 20]}
onGestureEvent={gestureControls.onBrightnessGestureEvent}
activeOffsetY={[-10, 10]}
failOffsetX={[-30, 30]}
shouldCancelWhenOutside={false}
simultaneousHandlers={[]}
maxPointers={1}
@ -2644,9 +2550,9 @@ const KSPlayerCore: React.FC = () => {
{/* Combined gesture handler for right side - volume + tap */}
<PanGestureHandler
onGestureEvent={onVolumeGestureEvent}
activeOffsetY={[-5, 5]}
failOffsetX={[-20, 20]}
onGestureEvent={gestureControls.onVolumeGestureEvent}
activeOffsetY={[-10, 10]}
failOffsetX={[-30, 30]}
shouldCancelWhenOutside={false}
simultaneousHandlers={[]}
maxPointers={1}
@ -2776,6 +2682,7 @@ const KSPlayerCore: React.FC = () => {
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 && (
<Animated.View
style={{
position: 'absolute',
left: getDimensions().width / 2 - 60,
top: getDimensions().height / 2 - 60,
opacity: volumeOverlayOpacity,
opacity: gestureControls.volumeOverlayOpacity,
zIndex: 1000,
}}
>
@ -3266,13 +3173,13 @@ const KSPlayerCore: React.FC = () => {
)}
{/* Brightness Overlay */}
{showBrightnessOverlay && (
{gestureControls.showBrightnessOverlay && (
<Animated.View
style={{
position: 'absolute',
left: getDimensions().width / 2 - 60,
top: getDimensions().height / 2 - 60,
opacity: brightnessOverlayOpacity,
opacity: gestureControls.brightnessOverlayOpacity,
zIndex: 1000,
}}
>

View file

@ -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<NodeJS.Timeout | null>(null);
const brightnessOverlayTimeout = useRef<NodeJS.Timeout | null>(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,
};
};