import React from 'react'; import { View, Text, TouchableOpacity, Animated, StyleSheet, Platform, Dimensions } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import Feather from 'react-native-vector-icons/Feather'; import { LinearGradient } from 'expo-linear-gradient'; import Slider from '@react-native-community/slider'; import { styles } from '../utils/playerStyles'; // Updated styles import { getTrackDisplayName } from '../utils/playerUtils'; import { useTheme } from '../../../contexts/ThemeContext'; interface PlayerControlsProps { showControls: boolean; fadeAnim: Animated.Value; paused: boolean; title: string; episodeTitle?: string; season?: number; episode?: number; quality?: string; year?: number; streamProvider?: string; streamName?: string; currentTime: number; duration: number; zoomScale: number; currentResizeMode?: string; ksAudioTracks: Array<{id: number, name: string, language?: string}>; selectedAudioTrack: number | null; availableStreams?: { [providerId: string]: { streams: any[]; addonName: string } }; togglePlayback: () => void; skip: (seconds: number) => void; handleClose: () => void; cycleAspectRatio: () => void; cyclePlaybackSpeed: () => void; currentPlaybackSpeed: number; setShowAudioModal: (show: boolean) => void; setShowSubtitleModal: (show: boolean) => void; setShowSpeedModal: (show: boolean) => void; isSubtitleModalOpen?: boolean; setShowSourcesModal?: (show: boolean) => void; // Slider-specific props onSliderValueChange: (value: number) => void; onSlidingStart: () => void; onSlidingComplete: (value: number) => void; buffered: number; formatTime: (seconds: number) => string; playerBackend?: string; // AirPlay props isAirPlayActive?: boolean; allowsAirPlay?: boolean; onAirPlayPress?: () => void; } export const PlayerControls: React.FC = ({ showControls, fadeAnim, paused, title, episodeTitle, season, episode, quality, year, streamProvider, streamName, currentTime, duration, zoomScale, currentResizeMode, ksAudioTracks, selectedAudioTrack, availableStreams, togglePlayback, skip, handleClose, cycleAspectRatio, cyclePlaybackSpeed, currentPlaybackSpeed, setShowAudioModal, setShowSubtitleModal, setShowSpeedModal, isSubtitleModalOpen, setShowSourcesModal, onSliderValueChange, onSlidingStart, onSlidingComplete, buffered, formatTime, playerBackend, isAirPlayActive, allowsAirPlay, onAirPlayPress, }) => { const { currentTheme } = useTheme(); /* Responsive Spacing */ const screenWidth = Dimensions.get('window').width; const buttonSpacing = screenWidth * 0.10; // Reduced from 15% to 10% const playButtonSize = screenWidth * 0.08; // 8% of screen width (reduced from 12%) const playIconSizeCalculated = playButtonSize * 0.6; // 60% of button size const seekButtonSize = screenWidth * 0.07; // 7% of screen width (reduced from 11%) 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(); }; const deviceWidth = Dimensions.get('window').width; const BREAKPOINTS = { phone: 0, tablet: 768, largeTablet: 1024, tv: 1440 } as const; const getDeviceType = (w: number) => { if (w >= BREAKPOINTS.tv) return 'tv'; if (w >= BREAKPOINTS.largeTablet) return 'largeTablet'; if (w >= BREAKPOINTS.tablet) return 'tablet'; return 'phone'; }; const deviceType = getDeviceType(deviceWidth); const isTablet = deviceType === 'tablet'; const isLargeTablet = deviceType === 'largeTablet'; const isTV = deviceType === 'tv'; const closeIconSize = isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20; const skipIconSize = isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20; const playIconSize = isTV ? 48 : isLargeTablet ? 40 : isTablet ? 36 : 32; return ( {/* Progress slider with native iOS slider */} {formatTime(currentTime)} {formatTime(duration)} {/* Controls Overlay */} {/* Top Gradient & Header */} {/* Title Section - Enhanced with metadata */} {title} {/* Show season and episode for series */} {season && episode && ( S{season}E{episode} {episodeTitle && `• ${episodeTitle}`} )} {/* Show year and provider (quality chip removed) */} {year && {year}} {streamName && via {streamName}} {playerBackend && ( {playerBackend} )} {/* AirPlay Button - iOS only, KSAVPlayer only */} {Platform.OS === 'ios' && onAirPlayPress && playerBackend === 'KSAVPlayer' && ( )} {/* Center Controls - CloudStream Style */} {/* Backward Seek Button (-10s) */} handleSeekWithAnimation(-10)} activeOpacity={0.7} > {showBackwardSign ? '-10' : '10'} {/* Play/Pause Button */} {/* Forward Seek Button (+10s) */} handleSeekWithAnimation(10)} activeOpacity={0.7} > {showForwardSign ? '+10' : '10'} {/* Bottom Gradient */} {/* Bottom Buttons Row */} {/* Aspect Ratio Button - uses official resize modes */} {currentResizeMode ? (currentResizeMode === 'none' ? 'Original' : currentResizeMode.charAt(0).toUpperCase() + currentResizeMode.slice(1)) : 'Contain'} {/* Playback Speed Button */} setShowSpeedModal(true)}> Speed {currentPlaybackSpeed}x {/* Audio Button - Updated to use ksAudioTracks */} setShowAudioModal(true)} disabled={ksAudioTracks.length <= 1} > {(() => { const trackName = getTrackDisplayName( ksAudioTracks.find(t => t.id === selectedAudioTrack) || { id: -1, name: 'Default' } ); // Truncate long audio track names to prevent UI cramping const maxLength = 12; // Limit to 12 characters return trackName.length > maxLength ? `${trackName.substring(0, maxLength)}...` : trackName; })()} {/* Subtitle Button - Always available for external subtitle search */} setShowSubtitleModal(!isSubtitleModalOpen)} > Subtitles {/* Change Source Button */} {setShowSourcesModal && ( setShowSourcesModal(true)} > Change Source )} ); }; export default PlayerControls;