improved gesture handler behaviour

This commit is contained in:
tapframe 2026-01-27 00:26:37 +05:30
parent a318bd350b
commit fd9cc1ac52
4 changed files with 78 additions and 891 deletions

View file

@ -1,464 +0,0 @@
import React, { useState } from 'react';
import { View, Text, StyleSheet, Animated } from 'react-native';
import {
TapGestureHandler,
PanGestureHandler,
LongPressGestureHandler,
State
} from 'react-native-gesture-handler';
import { MaterialIcons } from '@expo/vector-icons';
import { styles as localStyles } from '../../utils/playerStyles';
interface GestureControlsProps {
screenDimensions: { width: number, height: number };
gestureControls: any;
onLongPressActivated: () => void;
onLongPressEnd: () => void;
onLongPressStateChange: (event: any) => void;
toggleControls: () => void;
showControls: boolean;
hideControls: () => void;
volume: number;
brightness: number;
controlsTimeout: React.MutableRefObject<NodeJS.Timeout | null>;
resizeMode?: string;
// New props for double-tap skip and horizontal seek
skip?: (seconds: number) => void;
currentTime?: number;
duration?: number;
seekToTime?: (seconds: number) => void;
formatTime?: (seconds: number) => string;
}
export const GestureControls: React.FC<GestureControlsProps> = ({
screenDimensions,
gestureControls,
onLongPressActivated,
onLongPressEnd,
onLongPressStateChange,
toggleControls,
showControls,
hideControls,
volume,
brightness,
controlsTimeout,
resizeMode = 'contain',
skip,
currentTime,
duration,
seekToTime,
formatTime,
}) => {
const getVolumeIcon = (value: number) => {
if (value === 0) return 'volume-off';
if (value < 0.3) return 'volume-mute';
if (value < 0.6) return 'volume-down';
return 'volume-up';
};
const getBrightnessIcon = (value: number) => {
if (value < 0.3) return 'brightness-low';
if (value < 0.7) return 'brightness-medium';
return 'brightness-high';
};
// Refs for gesture handlers
const leftDoubleTapRef = React.useRef(null);
const rightDoubleTapRef = React.useRef(null);
const horizontalSeekPanRef = React.useRef(null);
const leftVerticalPanRef = React.useRef(null);
const rightVerticalPanRef = React.useRef(null);
// State for double-tap skip overlays
const [showSkipForwardOverlay, setShowSkipForwardOverlay] = useState(false);
const [showSkipBackwardOverlay, setShowSkipBackwardOverlay] = useState(false);
const [skipAmount, setSkipAmount] = useState(10);
// State for horizontal seek
const [isHorizontalSeeking, setIsHorizontalSeeking] = useState(false);
const [seekPreviewTime, setSeekPreviewTime] = useState(0);
const [seekStartTime, setSeekStartTime] = useState(0);
// Refs for overlay timeouts
const skipForwardTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
const skipBackwardTimeoutRef = React.useRef<NodeJS.Timeout | null>(null);
// Cleanup timeouts on unmount
React.useEffect(() => {
return () => {
if (skipForwardTimeoutRef.current) clearTimeout(skipForwardTimeoutRef.current);
if (skipBackwardTimeoutRef.current) clearTimeout(skipBackwardTimeoutRef.current);
};
}, []);
// Double-tap handlers
const handleLeftDoubleTap = () => {
if (skip) {
skip(-10);
setSkipAmount(prev => {
const newAmount = showSkipBackwardOverlay ? prev + 10 : 10;
return newAmount;
});
setShowSkipBackwardOverlay(true);
if (skipBackwardTimeoutRef.current) {
clearTimeout(skipBackwardTimeoutRef.current);
}
skipBackwardTimeoutRef.current = setTimeout(() => {
setShowSkipBackwardOverlay(false);
setSkipAmount(10);
}, 800);
}
};
const handleRightDoubleTap = () => {
if (skip) {
skip(10);
setSkipAmount(prev => {
const newAmount = showSkipForwardOverlay ? prev + 10 : 10;
return newAmount;
});
setShowSkipForwardOverlay(true);
if (skipForwardTimeoutRef.current) {
clearTimeout(skipForwardTimeoutRef.current);
}
skipForwardTimeoutRef.current = setTimeout(() => {
setShowSkipForwardOverlay(false);
setSkipAmount(10);
}, 800);
}
};
// Shared styles for gesture areas (relative to parent container)
const leftSideStyle = {
position: 'absolute' as const,
top: 0,
left: 0,
width: screenDimensions.width * 0.4,
height: '100%' as const,
};
const rightSideStyle = {
position: 'absolute' as const,
top: 0,
right: 0,
width: screenDimensions.width * 0.4,
height: '100%' as const,
};
// Full gesture area style
const gestureAreaStyle = {
position: 'absolute' as const,
top: screenDimensions.height * 0.15,
left: 0,
width: screenDimensions.width,
height: screenDimensions.height * 0.7,
zIndex: 10,
};
return (
<>
{/* Horizontal seek gesture - OUTERMOST wrapper, fails on vertical movement */}
<PanGestureHandler
ref={horizontalSeekPanRef}
onGestureEvent={(event: any) => {
const { translationX, state } = event.nativeEvent;
if (state === State.ACTIVE) {
if (!isHorizontalSeeking && currentTime !== undefined) {
setIsHorizontalSeeking(true);
setSeekStartTime(currentTime);
}
if (duration && duration > 0) {
const sensitivityFactor = duration > 3600 ? 120 : duration > 1800 ? 90 : 60;
const seekDelta = (translationX / screenDimensions.width) * sensitivityFactor;
const newTime = Math.max(0, Math.min(duration, seekStartTime + seekDelta));
setSeekPreviewTime(newTime);
}
}
}}
onHandlerStateChange={(event: any) => {
const { state } = event.nativeEvent;
if (state === State.END || state === State.CANCELLED) {
if (isHorizontalSeeking && seekToTime) {
seekToTime(seekPreviewTime);
}
setIsHorizontalSeeking(false);
}
}}
activeOffsetX={[-30, 30]}
failOffsetY={[-20, 20]}
maxPointers={1}
>
<View style={gestureAreaStyle}>
{/* Left side gestures */}
<TapGestureHandler
ref={leftDoubleTapRef}
numberOfTaps={2}
onActivated={handleLeftDoubleTap}
>
<View style={leftSideStyle}>
<LongPressGestureHandler
onActivated={onLongPressActivated}
onEnded={onLongPressEnd}
onHandlerStateChange={onLongPressStateChange}
minDurationMs={500}
>
<View style={StyleSheet.absoluteFill}>
<PanGestureHandler
ref={leftVerticalPanRef}
onGestureEvent={gestureControls.onBrightnessGestureEvent}
activeOffsetY={[-10, 10]}
failOffsetX={[-20, 20]}
maxPointers={1}
>
<View style={StyleSheet.absoluteFill}>
<TapGestureHandler
waitFor={leftDoubleTapRef}
onActivated={toggleControls}
>
<View style={StyleSheet.absoluteFill} />
</TapGestureHandler>
</View>
</PanGestureHandler>
</View>
</LongPressGestureHandler>
</View>
</TapGestureHandler>
{/* Center area tap handler */}
<TapGestureHandler
onActivated={() => {
if (showControls) {
const timeoutId = setTimeout(() => {
hideControls();
}, 0);
if (controlsTimeout.current) {
clearTimeout(controlsTimeout.current);
}
controlsTimeout.current = timeoutId;
} else {
toggleControls();
}
}}
>
<View style={{
position: 'absolute',
top: 0,
left: screenDimensions.width * 0.4,
width: screenDimensions.width * 0.2,
height: '100%',
}} />
</TapGestureHandler>
{/* Right side gestures */}
<TapGestureHandler
ref={rightDoubleTapRef}
numberOfTaps={2}
onActivated={handleRightDoubleTap}
>
<View style={rightSideStyle}>
<LongPressGestureHandler
onActivated={onLongPressActivated}
onEnded={onLongPressEnd}
onHandlerStateChange={onLongPressStateChange}
minDurationMs={500}
>
<View style={StyleSheet.absoluteFill}>
<PanGestureHandler
ref={rightVerticalPanRef}
onGestureEvent={gestureControls.onVolumeGestureEvent}
activeOffsetY={[-10, 10]}
failOffsetX={[-20, 20]}
maxPointers={1}
>
<View style={StyleSheet.absoluteFill}>
<TapGestureHandler
waitFor={rightDoubleTapRef}
onActivated={toggleControls}
>
<View style={StyleSheet.absoluteFill} />
</TapGestureHandler>
</View>
</PanGestureHandler>
</View>
</LongPressGestureHandler>
</View>
</TapGestureHandler>
</View>
</PanGestureHandler>
{/* Volume/Brightness Pill Overlay */}
{(gestureControls.showVolumeOverlay || gestureControls.showBrightnessOverlay) && (
<View style={localStyles.gestureIndicatorContainer}>
<View style={localStyles.gestureIndicatorPill}>
<View
style={[
localStyles.iconWrapper,
{
backgroundColor: gestureControls.showVolumeOverlay && volume === 0
? 'rgba(242, 184, 181)'
: 'rgba(59, 59, 59)'
}
]}
>
<MaterialIcons
name={
gestureControls.showVolumeOverlay
? getVolumeIcon(volume)
: getBrightnessIcon(brightness)
}
size={24}
color={
gestureControls.showVolumeOverlay && volume === 0
? 'rgba(96, 20, 16)'
: 'rgba(255, 255, 255)'
}
/>
</View>
<Text
style={[
localStyles.gestureText,
gestureControls.showVolumeOverlay && volume === 0 && { color: 'rgba(242, 184, 181)' }
]}
>
{gestureControls.showVolumeOverlay && volume === 0
? "Muted"
: `${Math.round((gestureControls.showVolumeOverlay ? volume : brightness) * 100)}%`
}
</Text>
</View>
</View>
)}
{/* Aspect Ratio Overlay */}
{gestureControls.showResizeModeOverlay && (
<View style={localStyles.gestureIndicatorContainer}>
<Animated.View
style={[
localStyles.gestureIndicatorPill,
{ opacity: gestureControls.resizeModeOverlayOpacity }
]}
>
<View
style={[
localStyles.iconWrapper,
{
backgroundColor: 'rgba(59, 59, 59)'
}
]}
>
<MaterialIcons
name="aspect-ratio"
size={24}
color="rgba(255, 255, 255)"
/>
</View>
<Text
style={localStyles.gestureText}
>
{resizeMode.charAt(0).toUpperCase() + resizeMode.slice(1)}
</Text>
</Animated.View>
</View>
)}
{/* Skip Forward Overlay - Right side */}
{showSkipForwardOverlay && (
<View style={{
position: 'absolute',
right: screenDimensions.width * 0.1,
top: screenDimensions.height * 0.35,
height: screenDimensions.height * 0.3,
width: screenDimensions.width * 0.25,
justifyContent: 'center',
alignItems: 'center',
zIndex: 2000,
}}>
<View style={{
position: 'absolute',
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: 'rgba(255, 255, 255, 0.15)',
}} />
<View style={{ alignItems: 'center' }}>
<MaterialIcons name="fast-forward" size={32} color="#FFFFFF" />
<Text style={{
color: '#FFFFFF',
fontSize: 16,
fontWeight: 'bold',
marginTop: 4,
}}>
+{skipAmount}s
</Text>
</View>
</View>
)}
{/* Skip Backward Overlay - Left side */}
{showSkipBackwardOverlay && (
<View style={{
position: 'absolute',
left: screenDimensions.width * 0.1,
top: screenDimensions.height * 0.35,
height: screenDimensions.height * 0.3,
width: screenDimensions.width * 0.25,
justifyContent: 'center',
alignItems: 'center',
zIndex: 2000,
}}>
<View style={{
position: 'absolute',
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: 'rgba(255, 255, 255, 0.15)',
}} />
<View style={{ alignItems: 'center' }}>
<MaterialIcons name="fast-rewind" size={32} color="#FFFFFF" />
<Text style={{
color: '#FFFFFF',
fontSize: 16,
fontWeight: 'bold',
marginTop: 4,
}}>
-{skipAmount}s
</Text>
</View>
</View>
)}
{/* Horizontal Seek Preview Overlay */}
{isHorizontalSeeking && formatTime && (
<View style={localStyles.gestureIndicatorContainer}>
<View style={localStyles.gestureIndicatorPill}>
<View style={[localStyles.iconWrapper, { backgroundColor: 'rgba(59, 59, 59)' }]}>
<MaterialIcons
name={seekPreviewTime > (currentTime || 0) ? "fast-forward" : "fast-rewind"}
size={24}
color="rgba(255, 255, 255)"
/>
</View>
<Text style={localStyles.gestureText}>
{formatTime(seekPreviewTime)}
</Text>
<Text style={{
color: seekPreviewTime > (currentTime || 0) ? '#4CAF50' : '#FF5722',
fontSize: 12,
fontWeight: '600',
marginLeft: 4,
}}>
{seekPreviewTime > (currentTime || 0) ? '+' : ''}
{Math.round(seekPreviewTime - (currentTime || 0))}s
</Text>
</View>
</View>
)}
</>
);
};

View file

@ -92,40 +92,85 @@ export const GestureControls: React.FC<GestureControlsProps> = ({
};
}, []);
// Refs for tracking rapid seek state
const seekBaselineTime = React.useRef<number | null>(null);
const gestureSkipAmount = React.useRef(0);
// Double-tap handlers
const handleLeftDoubleTap = () => {
if (skip) {
skip(-10);
setSkipAmount(prev => {
const newAmount = showSkipBackwardOverlay ? prev + 10 : 10;
return newAmount;
});
if (seekToTime && currentTime !== undefined) {
// If overlay is not visible, this is a new seek sequence
if (!showSkipBackwardOverlay) {
seekBaselineTime.current = currentTime;
gestureSkipAmount.current = 0;
}
// Increment skip amount
gestureSkipAmount.current += 10;
const currentSkip = gestureSkipAmount.current;
// Calculate target time based on locked baseline
const baseTime = seekBaselineTime.current !== null ? seekBaselineTime.current : currentTime;
const targetTime = Math.max(0, baseTime - currentSkip);
// Execute seek
seekToTime(targetTime);
// Update UI state
setSkipAmount(currentSkip);
setShowSkipBackwardOverlay(true);
if (skipBackwardTimeoutRef.current) {
clearTimeout(skipBackwardTimeoutRef.current);
}
skipBackwardTimeoutRef.current = setTimeout(() => {
setShowSkipBackwardOverlay(false);
setSkipAmount(10);
gestureSkipAmount.current = 0;
seekBaselineTime.current = null;
}, 800);
} else if (skip) {
// Fallback if seekToTime not available
skip(-10);
}
};
const handleRightDoubleTap = () => {
if (skip) {
skip(10);
setSkipAmount(prev => {
const newAmount = showSkipForwardOverlay ? prev + 10 : 10;
return newAmount;
});
if (seekToTime && currentTime !== undefined) {
// If overlay is not visible, this is a new seek sequence
if (!showSkipForwardOverlay) {
seekBaselineTime.current = currentTime;
gestureSkipAmount.current = 0;
}
// Increment skip amount
gestureSkipAmount.current += 10;
const currentSkip = gestureSkipAmount.current;
// Calculate target time based on locked baseline
const baseTime = seekBaselineTime.current !== null ? seekBaselineTime.current : currentTime;
const targetTime = baseTime + currentSkip;
// Note: duration check happens in seekToTime
// Execute seek
seekToTime(targetTime);
// Update UI state
setSkipAmount(currentSkip);
setShowSkipForwardOverlay(true);
if (skipForwardTimeoutRef.current) {
clearTimeout(skipForwardTimeoutRef.current);
}
skipForwardTimeoutRef.current = setTimeout(() => {
setShowSkipForwardOverlay(false);
setSkipAmount(10);
gestureSkipAmount.current = 0;
seekBaselineTime.current = null;
}, 800);
} else if (skip) {
// Fallback
skip(10);
}
};
@ -362,31 +407,12 @@ export const GestureControls: React.FC<GestureControlsProps> = ({
{/* Skip Forward Overlay - Right side */}
{showSkipForwardOverlay && (
<View style={{
position: 'absolute',
right: screenDimensions.width * 0.1,
top: screenDimensions.height * 0.35,
height: screenDimensions.height * 0.3,
width: screenDimensions.width * 0.25,
justifyContent: 'center',
alignItems: 'center',
zIndex: 2000,
}}>
<View style={{
position: 'absolute',
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: 'rgba(255, 255, 255, 0.15)',
}} />
<View style={{ alignItems: 'center' }}>
<MaterialIcons name="fast-forward" size={32} color="#FFFFFF" />
<Text style={{
color: '#FFFFFF',
fontSize: 16,
fontWeight: 'bold',
marginTop: 4,
}}>
<View style={localStyles.gestureIndicatorContainer}>
<View style={localStyles.gestureIndicatorPill}>
<View style={localStyles.iconWrapper}>
<MaterialIcons name="fast-forward" size={18} color="rgba(255, 255, 255, 0.9)" />
</View>
<Text style={localStyles.gestureText}>
+{skipAmount}s
</Text>
</View>
@ -395,31 +421,12 @@ export const GestureControls: React.FC<GestureControlsProps> = ({
{/* Skip Backward Overlay - Left side */}
{showSkipBackwardOverlay && (
<View style={{
position: 'absolute',
left: screenDimensions.width * 0.1,
top: screenDimensions.height * 0.35,
height: screenDimensions.height * 0.3,
width: screenDimensions.width * 0.25,
justifyContent: 'center',
alignItems: 'center',
zIndex: 2000,
}}>
<View style={{
position: 'absolute',
width: 100,
height: 100,
borderRadius: 50,
backgroundColor: 'rgba(255, 255, 255, 0.15)',
}} />
<View style={{ alignItems: 'center' }}>
<MaterialIcons name="fast-rewind" size={32} color="#FFFFFF" />
<Text style={{
color: '#FFFFFF',
fontSize: 16,
fontWeight: 'bold',
marginTop: 4,
}}>
<View style={localStyles.gestureIndicatorContainer}>
<View style={localStyles.gestureIndicatorPill}>
<View style={localStyles.iconWrapper}>
<MaterialIcons name="fast-rewind" size={18} color="rgba(255, 255, 255, 0.9)" />
</View>
<Text style={localStyles.gestureText}>
-{skipAmount}s
</Text>
</View>

View file

@ -37,30 +37,32 @@ export const usePlayerControls = (config: PlayerControlsConfig) => {
setPaused(!paused);
}, [paused, setPaused]);
const seekTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const seekToTime = useCallback((rawSeconds: number) => {
const timeInSeconds = Math.max(0, Math.min(rawSeconds, duration > 0 ? duration - END_EPSILON : rawSeconds));
if (playerRef.current && duration > 0 && !isSeeking.current) {
if (playerRef.current && duration > 0) {
if (DEBUG_MODE) logger.log(`[usePlayerControls] Seeking to ${timeInSeconds}`);
isSeeking.current = true;
// iOS optimization: pause while seeking for smoother experience
// Clear existing timeout to keep isSeeking true during rapid seeks
if (seekTimeoutRef.current) {
clearTimeout(seekTimeoutRef.current);
}
// Actually perform the seek
playerRef.current.seek(timeInSeconds);
// Debounce the seeking state reset
setTimeout(() => {
seekTimeoutRef.current = setTimeout(() => {
if (isMounted.current && isSeeking.current) {
isSeeking.current = false;
// Resume if it was playing (iOS specific)
}
}, 500);
}
}, [duration, paused, setPaused, playerRef, isSeeking, isMounted]);
}, [duration, paused, playerRef, isSeeking, isMounted]);
const skip = useCallback((seconds: number) => {
seekToTime(currentTime + seconds);

View file

@ -1,358 +0,0 @@
import React from 'react';
import { View, Text, Animated } from 'react-native';
import {
TapGestureHandler,
PanGestureHandler,
LongPressGestureHandler,
} from 'react-native-gesture-handler';
import { MaterialIcons } from '@expo/vector-icons';
interface GestureControlsProps {
screenDimensions: { width: number, height: number };
gestureControls: any;
onLongPressActivated: () => void;
onLongPressEnd: () => void;
onLongPressStateChange: (event: any) => void;
toggleControls: () => void;
showControls: boolean;
hideControls: () => void;
volume: number;
brightness: number;
controlsTimeout: React.MutableRefObject<NodeJS.Timeout | null>;
}
export const GestureControls: React.FC<GestureControlsProps> = ({
screenDimensions,
gestureControls,
onLongPressActivated,
onLongPressEnd,
onLongPressStateChange,
toggleControls,
showControls,
hideControls,
volume,
brightness,
controlsTimeout
}) => {
// Helper to get dimensions (using passed screenDimensions)
const getDimensions = () => screenDimensions;
// Create refs for all gesture handlers to enable cross-referencing
const leftPanRef = React.useRef(null);
const rightPanRef = React.useRef(null);
const leftTapRef = React.useRef(null);
const rightTapRef = React.useRef(null);
const centerTapRef = React.useRef(null);
const leftLongPressRef = React.useRef(null);
const rightLongPressRef = React.useRef(null);
// Shared style for left side gesture area
const leftSideStyle = {
position: 'absolute' as const,
top: screenDimensions.height * 0.15,
left: 0,
width: screenDimensions.width * 0.4,
height: screenDimensions.height * 0.7,
zIndex: 10,
};
// Shared style for right side gesture area
const rightSideStyle = {
position: 'absolute' as const,
top: screenDimensions.height * 0.15,
right: 0,
width: screenDimensions.width * 0.4,
height: screenDimensions.height * 0.7,
zIndex: 10,
};
return (
<>
{/* Left side gestures - brightness + tap + long press (flat structure) */}
<LongPressGestureHandler
ref={leftLongPressRef}
onActivated={onLongPressActivated}
onEnded={onLongPressEnd}
onHandlerStateChange={onLongPressStateChange}
minDurationMs={500}
shouldCancelWhenOutside={false}
>
<View style={leftSideStyle} />
</LongPressGestureHandler>
<PanGestureHandler
ref={leftPanRef}
onGestureEvent={gestureControls.onBrightnessGestureEvent}
activeOffsetY={[-15, 15]}
failOffsetX={[-60, 60]}
shouldCancelWhenOutside={false}
maxPointers={1}
>
<View style={leftSideStyle} />
</PanGestureHandler>
<TapGestureHandler
ref={leftTapRef}
onActivated={toggleControls}
shouldCancelWhenOutside={false}
waitFor={[leftPanRef, leftLongPressRef]}
>
<View style={leftSideStyle} />
</TapGestureHandler>
{/* Right side gestures - volume + tap + long press (flat structure) */}
<LongPressGestureHandler
ref={rightLongPressRef}
onActivated={onLongPressActivated}
onEnded={onLongPressEnd}
onHandlerStateChange={onLongPressStateChange}
minDurationMs={500}
shouldCancelWhenOutside={false}
>
<View style={rightSideStyle} />
</LongPressGestureHandler>
<PanGestureHandler
ref={rightPanRef}
onGestureEvent={gestureControls.onVolumeGestureEvent}
activeOffsetY={[-15, 15]}
failOffsetX={[-60, 60]}
shouldCancelWhenOutside={false}
maxPointers={1}
>
<View style={rightSideStyle} />
</PanGestureHandler>
<TapGestureHandler
ref={rightTapRef}
onActivated={toggleControls}
shouldCancelWhenOutside={false}
waitFor={[rightPanRef, rightLongPressRef]}
>
<View style={rightSideStyle} />
</TapGestureHandler>
{/* Center area tap handler */}
<TapGestureHandler
ref={centerTapRef}
onActivated={() => {
if (showControls) {
const timeoutId = setTimeout(() => {
hideControls();
}, 0);
if (controlsTimeout.current) {
clearTimeout(controlsTimeout.current);
}
controlsTimeout.current = timeoutId;
} else {
toggleControls();
}
}}
shouldCancelWhenOutside={false}
>
<View style={{
position: 'absolute',
top: screenDimensions.height * 0.15,
left: screenDimensions.width * 0.4,
width: screenDimensions.width * 0.2,
height: screenDimensions.height * 0.7,
zIndex: 10,
}} />
</TapGestureHandler>
{/* Volume Overlay */}
{gestureControls.showVolumeOverlay && (
<Animated.View
style={{
position: 'absolute',
left: getDimensions().width / 2 - 60,
top: getDimensions().height / 2 - 60,
opacity: gestureControls.volumeOverlayOpacity,
zIndex: 1000,
}}
>
<View style={{
backgroundColor: 'rgba(0, 0, 0, 0.9)',
borderRadius: 12,
padding: 16,
alignItems: 'center',
width: 120,
height: 120,
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.5,
shadowRadius: 8,
elevation: 10,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
}}>
<MaterialIcons
name={volume === 0 ? "volume-off" : volume < 30 ? "volume-mute" : volume < 70 ? "volume-down" : "volume-up"}
size={24}
color={volume === 0 ? "#FF6B6B" : "#FFFFFF"}
style={{ marginBottom: 8 }}
/>
{/* Horizontal Dotted Progress Bar */}
<View style={{
width: 80,
height: 6,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden',
marginBottom: 8,
}}>
{/* Dotted background */}
<View style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 1,
}}>
{Array.from({ length: 16 }, (_, i) => (
<View
key={i}
style={{
width: 1.5,
height: 1.5,
backgroundColor: 'rgba(255, 255, 255, 0.3)',
borderRadius: 0.75,
}}
/>
))}
</View>
{/* Progress fill */}
<View style={{
position: 'absolute',
top: 0,
left: 0,
width: `${volume}%`,
height: 6,
backgroundColor: volume === 0 ? '#FF6B6B' : '#E50914',
borderRadius: 3,
shadowColor: volume === 0 ? '#FF6B6B' : '#E50914',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.6,
shadowRadius: 2,
}} />
</View>
<Text style={{
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
letterSpacing: 0.5,
}}>
{Math.round(volume)}%
</Text>
</View>
</Animated.View>
)}
{/* Brightness Overlay */}
{gestureControls.showBrightnessOverlay && (
<Animated.View
style={{
position: 'absolute',
left: getDimensions().width / 2 - 60,
top: getDimensions().height / 2 - 60,
opacity: gestureControls.brightnessOverlayOpacity,
zIndex: 1000,
}}
>
<View style={{
backgroundColor: 'rgba(0, 0, 0, 0.9)',
borderRadius: 12,
padding: 16,
alignItems: 'center',
width: 120,
height: 120,
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.5,
shadowRadius: 8,
elevation: 10,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
}}>
<MaterialIcons
name={brightness < 0.2 ? "brightness-low" : brightness < 0.5 ? "brightness-medium" : brightness < 0.8 ? "brightness-high" : "brightness-auto"}
size={24}
color={brightness < 0.2 ? "#FFD700" : "#FFFFFF"}
style={{ marginBottom: 8 }}
/>
{/* Horizontal Dotted Progress Bar */}
<View style={{
width: 80,
height: 6,
backgroundColor: 'rgba(255, 255, 255, 0.2)',
borderRadius: 3,
position: 'relative',
overflow: 'hidden',
marginBottom: 8,
}}>
{/* Dotted background */}
<View style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 1,
}}>
{Array.from({ length: 16 }, (_, i) => (
<View
key={i}
style={{
width: 1.5,
height: 1.5,
backgroundColor: 'rgba(255, 255, 255, 0.3)',
borderRadius: 0.75,
}}
/>
))}
</View>
{/* Progress fill */}
<View style={{
position: 'absolute',
top: 0,
left: 0,
width: `${brightness * 100}%`,
height: 6,
backgroundColor: brightness < 0.2 ? '#FFD700' : '#FFA500',
borderRadius: 3,
shadowColor: brightness < 0.2 ? '#FFD700' : '#FFA500',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.6,
shadowRadius: 2,
}} />
</View>
<Text style={{
color: '#FFFFFF',
fontSize: 12,
fontWeight: '600',
letterSpacing: 0.5,
}}>
{Math.round(brightness * 100)}%
</Text>
</View>
</Animated.View>
)}
</>
);
};