diff --git a/src/components/player/controls/PlayerControls.tsx b/src/components/player/controls/PlayerControls.tsx index 0588d76..420aa37 100644 --- a/src/components/player/controls/PlayerControls.tsx +++ b/src/components/player/controls/PlayerControls.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { View, Text, TouchableOpacity, Animated, StyleSheet, Platform } from 'react-native'; +import { View, Text, TouchableOpacity, Animated, StyleSheet, Platform, Dimensions } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import Slider from '@react-native-community/slider'; @@ -82,6 +82,156 @@ export const PlayerControls: React.FC = ({ playerBackend, }) => { const { currentTheme } = useTheme(); + + + /* Responsive Spacing */ + const screenWidth = Dimensions.get('window').width; + const buttonSpacing = screenWidth * 0.15; + + const playButtonSize = screenWidth * 0.12; // 12% of screen width + const playIconSize = playButtonSize * 0.6; // 60% of button size + const seekButtonSize = screenWidth * 0.11; // 11% of screen width + const seekIconSize = seekButtonSize * 0.75; // 75% of button size + const seekNumberSize = seekButtonSize * 0.25; // 25% of button size + const arcBorderWidth = seekButtonSize * 0.05; // 5% of button size + + /* Animations - State & Refs */ + const [showBackwardSign, setShowBackwardSign] = React.useState(false); + const [showForwardSign, setShowForwardSign] = React.useState(false); + + /* Separate Animations for Each Button */ + const backwardPressAnim = React.useRef(new Animated.Value(0)).current; + const backwardSlideAnim = React.useRef(new Animated.Value(0)).current; + const backwardScaleAnim = React.useRef(new Animated.Value(1)).current; + const backwardArcOpacity = React.useRef(new Animated.Value(0)).current; + const backwardArcRotation = React.useRef(new Animated.Value(0)).current; + + const forwardPressAnim = React.useRef(new Animated.Value(0)).current; + const forwardSlideAnim = React.useRef(new Animated.Value(0)).current; + const forwardScaleAnim = React.useRef(new Animated.Value(1)).current; + const forwardArcOpacity = React.useRef(new Animated.Value(0)).current; + const forwardArcRotation = React.useRef(new Animated.Value(0)).current; + + const playPressAnim = React.useRef(new Animated.Value(0)).current; + const playIconScale = React.useRef(new Animated.Value(1)).current; + const playIconOpacity = React.useRef(new Animated.Value(1)).current; + + /* Handle Seek with Animation */ + const handleSeekWithAnimation = (seconds: number) => { + const isForward = seconds > 0; + + if (isForward) { + setShowForwardSign(true); + } else { + setShowBackwardSign(true); + } + + const pressAnim = isForward ? forwardPressAnim : backwardPressAnim; + const slideAnim = isForward ? forwardSlideAnim : backwardSlideAnim; + const scaleAnim = isForward ? forwardScaleAnim : backwardScaleAnim; + const arcOpacity = isForward ? forwardArcOpacity : backwardArcOpacity; + const arcRotation = isForward ? forwardArcRotation : backwardArcRotation; + + Animated.parallel([ + // Button press effect (circle flash) + Animated.sequence([ + Animated.timing(pressAnim, { + toValue: 1, + duration: 100, + useNativeDriver: true, + }), + Animated.timing(pressAnim, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + ]), + // Number slide out + Animated.sequence([ + Animated.timing(slideAnim, { + toValue: isForward ? (seekButtonSize * 0.75) : -(seekButtonSize * 0.75), + duration: 250, + useNativeDriver: true, + }), + Animated.timing(slideAnim, { + toValue: 0, + duration: 120, + useNativeDriver: true, + }), + ]), + // Button scale pulse + Animated.sequence([ + Animated.timing(scaleAnim, { + toValue: 1.15, + duration: 150, + useNativeDriver: true, + }), + Animated.timing(scaleAnim, { + toValue: 1, + duration: 150, + useNativeDriver: true, + }), + ]), + // Arc sweep animation + Animated.parallel([ + Animated.timing(arcOpacity, { + toValue: 1, + duration: 50, + useNativeDriver: true, + }), + Animated.timing(arcRotation, { + toValue: 1, + duration: 200, + useNativeDriver: true, + }), + ]), + ]).start(() => { + if (isForward) { + setShowForwardSign(false); + } else { + setShowBackwardSign(false); + } + arcOpacity.setValue(0); + arcRotation.setValue(0); + }); + + skip(seconds); + }; + + /* Handle Play/Pause with Animation */ + const handlePlayPauseWithAnimation = () => { + Animated.sequence([ + Animated.timing(playPressAnim, { + toValue: 1, + duration: 100, + useNativeDriver: true, + }), + Animated.timing(playPressAnim, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + ]).start(); + + Animated.sequence([ + Animated.timing(playIconScale, { + toValue: 0.85, + duration: 150, + useNativeDriver: true, + }), + Animated.timing(playIconScale, { + toValue: 1, + duration: 150, + useNativeDriver: true, + }), + ]).start(); + + togglePlayback(); + }; + + + + return ( = ({ - {/* Center Controls (Play/Pause, Skip) */} - - {/* Left Skip Button */} - skip(-10)} style={styles.skipButton}> - - - - 10 + + {/* Center Controls - CloudStream Style */} + + + {/* Backward Seek Button (-10s) */} + handleSeekWithAnimation(-10)} + activeOpacity={0.7} + > + + + + + + {showBackwardSign ? '-10' : '10'} + + + + + + {/* Play/Pause Button */} - - + + + + + + + - {/* Right Skip Button */} - skip(10)} style={styles.skipButton}> - - - - 10 + {/* Forward Seek Button (+10s) */} + handleSeekWithAnimation(10)} + activeOpacity={0.7} + > + + + + + + {showForwardSign ? '+10' : '10'} + + + + + + + + + + {/* Bottom Gradient */}