diff --git a/src/components/player/modals/SourcesModal.tsx b/src/components/player/modals/SourcesModal.tsx index 71b9b44a..1ed50356 100644 --- a/src/components/player/modals/SourcesModal.tsx +++ b/src/components/player/modals/SourcesModal.tsx @@ -1,6 +1,28 @@ import React from 'react'; -import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator } from 'react-native'; +import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Dimensions } from 'react-native'; import { Ionicons, MaterialIcons } from '@expo/vector-icons'; +import { BlurView } from 'expo-blur'; +import Animated, { + FadeIn, + FadeOut, + SlideInDown, + SlideOutDown, + FadeInDown, + FadeInUp, + Layout, + withSpring, + withTiming, + useAnimatedStyle, + useSharedValue, + interpolate, + Easing, + withDelay, + withSequence, + runOnJS, + BounceIn, + ZoomIn +} from 'react-native-reanimated'; +import { LinearGradient } from 'expo-linear-gradient'; import { styles } from '../utils/playerStyles'; import { Stream } from '../../../types/streams'; import QualityBadge from '../../metadata/QualityBadge'; @@ -14,6 +36,104 @@ interface SourcesModalProps { isChangingSource: boolean; } +const { width, height } = Dimensions.get('window'); + +const QualityIndicator = ({ quality }: { quality: string | null }) => { + if (!quality) return null; + + const qualityNum = parseInt(quality); + let color = '#8B5CF6'; // Default purple + let label = `${quality}p`; + + if (qualityNum >= 2160) { + color = '#F59E0B'; // Gold for 4K + label = '4K'; + } else if (qualityNum >= 1080) { + color = '#EF4444'; // Red for 1080p + label = 'FHD'; + } else if (qualityNum >= 720) { + color = '#10B981'; // Green for 720p + label = 'HD'; + } + + return ( + + + + {label} + + + ); +}; + +const StreamMetaBadge = ({ + text, + color, + bgColor, + icon, + delay = 0 +}: { + text: string; + color: string; + bgColor: string; + icon?: string; + delay?: number; +}) => ( + + {icon && ( + + )} + + {text} + + +); + const SourcesModal: React.FC = ({ showSourcesModal, setShowSourcesModal, @@ -22,6 +142,28 @@ const SourcesModal: React.FC = ({ onSelectStream, isChangingSource, }) => { + const modalScale = useSharedValue(0.9); + const modalOpacity = useSharedValue(0); + + React.useEffect(() => { + if (showSourcesModal) { + modalScale.value = withSpring(1, { + damping: 20, + stiffness: 300, + mass: 0.8, + }); + modalOpacity.value = withTiming(1, { + duration: 200, + easing: Easing.out(Easing.quad), + }); + } + }, [showSourcesModal]); + + const modalStyle = useAnimatedStyle(() => ({ + transform: [{ scale: modalScale.value }], + opacity: modalOpacity.value, + })); + if (!showSourcesModal) return null; const sortedProviders = Object.entries(availableStreams).sort(([a], [b]) => { @@ -47,113 +189,452 @@ const SourcesModal: React.FC = ({ return stream.url === currentStreamUrl; }; + const handleClose = () => { + modalScale.value = withTiming(0.9, { duration: 150 }); + modalOpacity.value = withTiming(0, { duration: 150 }); + setTimeout(() => setShowSourcesModal(false), 150); + }; + return ( - - - - Choose Source - setShowSourcesModal(false)} + + {/* Backdrop */} + + + {/* Modal Content */} + + {/* Glassmorphism Background */} + + {/* Header */} + - - - + + + Switch Source + + + Choose from {Object.values(availableStreams).reduce((acc, curr) => acc + curr.streams.length, 0)} available streams + + + + + + + + + - - {sortedProviders.map(([providerId, { streams, addonName }]) => ( - - {addonName} - - {streams.map((stream, index) => { - const quality = getQualityFromTitle(stream.title); - const isSelected = isStreamSelected(stream); - const isHDR = stream.title?.toLowerCase().includes('hdr'); - const isDolby = stream.title?.toLowerCase().includes('dolby') || stream.title?.includes('DV'); - const size = stream.title?.match(/💾\s*([\d.]+\s*[GM]B)/)?.[1]; - const isDebrid = stream.behaviorHints?.cached; - const isHDRezka = providerId === 'hdrezka'; + {/* Content */} + + {sortedProviders.map(([providerId, { streams, addonName }], providerIndex) => ( + 0 ? 32 : 0, + }} + > + {/* Provider Header */} + + + + + {addonName} + + + Provider • {streams.length} stream{streams.length !== 1 ? 's' : ''} + + + + + + {streams.length} + + + + + {/* Streams Grid */} + + {streams.map((stream, index) => { + const quality = getQualityFromTitle(stream.title); + const isSelected = isStreamSelected(stream); + const isHDR = stream.title?.toLowerCase().includes('hdr'); + const isDolby = stream.title?.toLowerCase().includes('dolby') || stream.title?.includes('DV'); + const size = stream.title?.match(/💾\s*([\d.]+\s*[GM]B)/)?.[1]; + const isDebrid = stream.behaviorHints?.cached; + const isHDRezka = providerId === 'hdrezka'; - return ( - handleStreamSelect(stream)} - disabled={isChangingSource || isSelected} - activeOpacity={0.7} - > - - - - {isHDRezka ? `HDRezka ${stream.title}` : (stream.name || stream.title || 'Unnamed Stream')} - - - {isSelected && ( - - - Current + return ( + + handleStreamSelect(stream)} + disabled={isChangingSource || isSelected} + activeOpacity={0.85} + > + + {/* Stream Info */} + + {/* Title Row */} + + + {isHDRezka ? `HDRezka ${stream.title}` : (stream.name || stream.title || 'Unnamed Stream')} + + + {isSelected && ( + + + + PLAYING + + + )} + + {isChangingSource && isSelected && ( + + + + Switching... + + + )} + + + {/* Subtitle */} + {!isHDRezka && stream.title && stream.title !== stream.name && ( + + {stream.title} + + )} + + {/* Enhanced Meta Info */} + + + + {isDolby && ( + + )} + + {isHDR && ( + + )} + + {size && ( + + )} + + {isDebrid && ( + + )} + + {isHDRezka && ( + + )} + + + + {/* Enhanced Action Icon */} + + {isSelected ? ( + + + + ) : ( + + )} + - )} - - {isChangingSource && isSelected && ( - - )} - - - {!isHDRezka && stream.title && stream.title !== stream.name && ( - {stream.title} - )} - - - {quality && quality >= "720" && ( - - )} - - {isDolby && ( - - )} - - {size && ( - - {size} - - )} - - {isDebrid && ( - - DEBRID - - )} - - {isHDRezka && ( - - HDREZKA - - )} - - - - - {isSelected ? ( - - ) : ( - - )} - - - ); - })} - - ))} - - - + + + ); + })} + + + ))} + + + + ); };