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 ? (
-
- ) : (
-
- )}
-
-
- );
- })}
-
- ))}
-
-
-
+
+
+ );
+ })}
+
+
+ ))}
+
+
+
+
);
};