mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
Enhance SourcesModal with improved animations and visual elements
This update introduces new animated components and visual enhancements to the SourcesModal, including a quality indicator and stream meta badges for better clarity on stream attributes. The modal now features a glassmorphism background and improved layout for a more engaging user experience. Additionally, the closing animation has been refined for smoother transitions, enhancing overall usability.
This commit is contained in:
parent
66fe4b748d
commit
9bf0bd2d9a
1 changed files with 584 additions and 103 deletions
|
|
@ -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 (
|
||||
<Animated.View
|
||||
entering={ZoomIn.duration(200).delay(100)}
|
||||
style={{
|
||||
backgroundColor: `${color}20`,
|
||||
borderColor: `${color}60`,
|
||||
borderWidth: 1,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<View style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: color,
|
||||
marginRight: 4,
|
||||
}} />
|
||||
<Text style={{
|
||||
color: color,
|
||||
fontSize: 10,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.5,
|
||||
}}>
|
||||
{label}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
const StreamMetaBadge = ({
|
||||
text,
|
||||
color,
|
||||
bgColor,
|
||||
icon,
|
||||
delay = 0
|
||||
}: {
|
||||
text: string;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
icon?: string;
|
||||
delay?: number;
|
||||
}) => (
|
||||
<Animated.View
|
||||
entering={FadeInUp.duration(200).delay(delay)}
|
||||
style={{
|
||||
backgroundColor: bgColor,
|
||||
borderColor: `${color}40`,
|
||||
borderWidth: 1,
|
||||
paddingHorizontal: 6,
|
||||
paddingVertical: 2,
|
||||
borderRadius: 6,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
elevation: 2,
|
||||
shadowColor: color,
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 2,
|
||||
}}
|
||||
>
|
||||
{icon && (
|
||||
<MaterialIcons name={icon as any} size={10} color={color} style={{ marginRight: 2 }} />
|
||||
)}
|
||||
<Text style={{
|
||||
color: color,
|
||||
fontSize: 9,
|
||||
fontWeight: '800',
|
||||
letterSpacing: 0.3,
|
||||
}}>
|
||||
{text}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
);
|
||||
|
||||
const SourcesModal: React.FC<SourcesModalProps> = ({
|
||||
showSourcesModal,
|
||||
setShowSourcesModal,
|
||||
|
|
@ -22,6 +142,28 @@ const SourcesModal: React.FC<SourcesModalProps> = ({
|
|||
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<SourcesModalProps> = ({
|
|||
return stream.url === currentStreamUrl;
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
modalScale.value = withTiming(0.9, { duration: 150 });
|
||||
modalOpacity.value = withTiming(0, { duration: 150 });
|
||||
setTimeout(() => setShowSourcesModal(false), 150);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.sourcesModal}>
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>Choose Source</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.modalCloseButton}
|
||||
onPress={() => setShowSourcesModal(false)}
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(250)}
|
||||
exiting={FadeOut.duration(200)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
zIndex: 9999,
|
||||
padding: 16,
|
||||
}}
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
onPress={handleClose}
|
||||
activeOpacity={1}
|
||||
/>
|
||||
|
||||
{/* Modal Content */}
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
width: Math.min(width - 32, 520),
|
||||
maxHeight: height * 0.85,
|
||||
overflow: 'hidden',
|
||||
elevation: 25,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 12 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 25,
|
||||
},
|
||||
modalStyle,
|
||||
]}
|
||||
>
|
||||
{/* Glassmorphism Background */}
|
||||
<BlurView
|
||||
intensity={100}
|
||||
tint="dark"
|
||||
style={{
|
||||
borderRadius: 28,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'rgba(26, 26, 26, 0.8)',
|
||||
}}
|
||||
>
|
||||
{/* Header */}
|
||||
<LinearGradient
|
||||
colors={[
|
||||
'rgba(229, 9, 20, 0.95)',
|
||||
'rgba(176, 6, 16, 0.95)',
|
||||
'rgba(139, 5, 12, 0.9)'
|
||||
]}
|
||||
locations={[0, 0.6, 1]}
|
||||
style={{
|
||||
paddingHorizontal: 28,
|
||||
paddingVertical: 24,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="close" size={24} color="white" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Animated.View
|
||||
entering={FadeInDown.duration(300).delay(100)}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Text style={{
|
||||
color: '#fff',
|
||||
fontSize: 24,
|
||||
fontWeight: '800',
|
||||
letterSpacing: -0.8,
|
||||
textShadowColor: 'rgba(0, 0, 0, 0.3)',
|
||||
textShadowOffset: { width: 0, height: 1 },
|
||||
textShadowRadius: 2,
|
||||
}}>
|
||||
Switch Source
|
||||
</Text>
|
||||
<Text style={{
|
||||
color: 'rgba(255, 255, 255, 0.85)',
|
||||
fontSize: 14,
|
||||
marginTop: 4,
|
||||
fontWeight: '500',
|
||||
letterSpacing: 0.2,
|
||||
}}>
|
||||
Choose from {Object.values(availableStreams).reduce((acc, curr) => acc + curr.streams.length, 0)} available streams
|
||||
</Text>
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View entering={BounceIn.duration(400).delay(200)}>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.15)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginLeft: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
}}
|
||||
onPress={handleClose}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons name="close" size={20} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
</LinearGradient>
|
||||
|
||||
<ScrollView style={styles.sourcesScrollView} showsVerticalScrollIndicator={false}>
|
||||
{sortedProviders.map(([providerId, { streams, addonName }]) => (
|
||||
<View key={providerId} style={styles.sourceProviderSection}>
|
||||
<Text style={styles.sourceProviderTitle}>{addonName}</Text>
|
||||
|
||||
{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 */}
|
||||
<ScrollView
|
||||
style={{
|
||||
maxHeight: height * 0.6,
|
||||
backgroundColor: 'transparent',
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
padding: 24,
|
||||
paddingBottom: 32,
|
||||
}}
|
||||
bounces={false}
|
||||
>
|
||||
{sortedProviders.map(([providerId, { streams, addonName }], providerIndex) => (
|
||||
<Animated.View
|
||||
key={providerId}
|
||||
entering={FadeInDown.duration(400).delay(150 + (providerIndex * 80))}
|
||||
layout={Layout.springify()}
|
||||
style={{
|
||||
marginBottom: streams.length > 0 ? 32 : 0,
|
||||
}}
|
||||
>
|
||||
{/* Provider Header */}
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 20,
|
||||
paddingBottom: 12,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(255, 255, 255, 0.08)',
|
||||
}}>
|
||||
<LinearGradient
|
||||
colors={providerId === 'hdrezka' ? ['#00d4aa', '#00a085'] : ['#E50914', '#B00610']}
|
||||
style={{
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 6,
|
||||
marginRight: 16,
|
||||
elevation: 3,
|
||||
shadowColor: providerId === 'hdrezka' ? '#00d4aa' : '#E50914',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 4,
|
||||
}}
|
||||
/>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{
|
||||
color: '#fff',
|
||||
fontSize: 18,
|
||||
fontWeight: '700',
|
||||
letterSpacing: -0.3,
|
||||
}}>
|
||||
{addonName}
|
||||
</Text>
|
||||
<Text style={{
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
fontSize: 12,
|
||||
marginTop: 1,
|
||||
fontWeight: '500',
|
||||
}}>
|
||||
Provider • {streams.length} stream{streams.length !== 1 ? 's' : ''}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
}}>
|
||||
<Text style={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize: 11,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.5,
|
||||
}}>
|
||||
{streams.length}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Streams Grid */}
|
||||
<View style={{ gap: 16 }}>
|
||||
{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 (
|
||||
<TouchableOpacity
|
||||
key={`${stream.url}-${index}`}
|
||||
style={[
|
||||
styles.sourceStreamItem,
|
||||
isSelected && styles.sourceStreamItemSelected
|
||||
]}
|
||||
onPress={() => handleStreamSelect(stream)}
|
||||
disabled={isChangingSource || isSelected}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.sourceStreamDetails}>
|
||||
<View style={styles.sourceStreamTitleRow}>
|
||||
<Text style={[
|
||||
styles.sourceStreamTitle,
|
||||
isSelected && styles.sourceStreamTitleSelected
|
||||
]}>
|
||||
{isHDRezka ? `HDRezka ${stream.title}` : (stream.name || stream.title || 'Unnamed Stream')}
|
||||
</Text>
|
||||
|
||||
{isSelected && (
|
||||
<View style={styles.currentStreamBadge}>
|
||||
<MaterialIcons name="play-arrow" size={16} color="#E50914" />
|
||||
<Text style={styles.currentSourceItem}>Current</Text>
|
||||
return (
|
||||
<Animated.View
|
||||
key={`${stream.url}-${index}`}
|
||||
entering={FadeInDown.duration(300).delay((providerIndex * 80) + (index * 40))}
|
||||
layout={Layout.springify()}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
backgroundColor: isSelected
|
||||
? 'rgba(229, 9, 20, 0.08)'
|
||||
: 'rgba(255, 255, 255, 0.03)',
|
||||
borderRadius: 20,
|
||||
padding: 20,
|
||||
borderWidth: 2,
|
||||
borderColor: isSelected
|
||||
? 'rgba(229, 9, 20, 0.4)'
|
||||
: 'rgba(255, 255, 255, 0.08)',
|
||||
elevation: isSelected ? 8 : 3,
|
||||
shadowColor: isSelected ? '#E50914' : '#000',
|
||||
shadowOffset: { width: 0, height: isSelected ? 4 : 2 },
|
||||
shadowOpacity: isSelected ? 0.3 : 0.1,
|
||||
shadowRadius: isSelected ? 12 : 6,
|
||||
transform: [{ scale: isSelected ? 1.02 : 1 }],
|
||||
}}
|
||||
onPress={() => handleStreamSelect(stream)}
|
||||
disabled={isChangingSource || isSelected}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
}}>
|
||||
{/* Stream Info */}
|
||||
<View style={{ flex: 1, marginRight: 16 }}>
|
||||
{/* Title Row */}
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 12,
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
}}>
|
||||
<Text style={{
|
||||
color: isSelected ? '#fff' : 'rgba(255, 255, 255, 0.95)',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
letterSpacing: -0.2,
|
||||
flex: 1,
|
||||
lineHeight: 22,
|
||||
}}>
|
||||
{isHDRezka ? `HDRezka ${stream.title}` : (stream.name || stream.title || 'Unnamed Stream')}
|
||||
</Text>
|
||||
|
||||
{isSelected && (
|
||||
<Animated.View
|
||||
entering={BounceIn.duration(300)}
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(229, 9, 20, 0.25)',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 5,
|
||||
borderRadius: 14,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(229, 9, 20, 0.5)',
|
||||
elevation: 4,
|
||||
shadowColor: '#E50914',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 4,
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="play-circle-filled" size={12} color="#E50914" />
|
||||
<Text style={{
|
||||
color: '#E50914',
|
||||
fontSize: 10,
|
||||
fontWeight: '800',
|
||||
marginLeft: 3,
|
||||
letterSpacing: 0.3,
|
||||
}}>
|
||||
PLAYING
|
||||
</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
{isChangingSource && isSelected && (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(200)}
|
||||
style={{
|
||||
backgroundColor: 'rgba(229, 9, 20, 0.2)',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator size="small" color="#E50914" />
|
||||
<Text style={{
|
||||
color: '#E50914',
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
marginLeft: 4,
|
||||
}}>
|
||||
Switching...
|
||||
</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Subtitle */}
|
||||
{!isHDRezka && stream.title && stream.title !== stream.name && (
|
||||
<Text style={{
|
||||
color: 'rgba(255, 255, 255, 0.65)',
|
||||
fontSize: 13,
|
||||
marginBottom: 12,
|
||||
lineHeight: 18,
|
||||
fontWeight: '400',
|
||||
}}>
|
||||
{stream.title}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Enhanced Meta Info */}
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 6,
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
<QualityIndicator quality={quality} />
|
||||
|
||||
{isDolby && (
|
||||
<StreamMetaBadge
|
||||
text="DOLBY"
|
||||
color="#8B5CF6"
|
||||
bgColor="rgba(139, 92, 246, 0.15)"
|
||||
icon="hd"
|
||||
delay={100}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isHDR && (
|
||||
<StreamMetaBadge
|
||||
text="HDR"
|
||||
color="#F59E0B"
|
||||
bgColor="rgba(245, 158, 11, 0.15)"
|
||||
icon="brightness-high"
|
||||
delay={120}
|
||||
/>
|
||||
)}
|
||||
|
||||
{size && (
|
||||
<StreamMetaBadge
|
||||
text={size}
|
||||
color="#6B7280"
|
||||
bgColor="rgba(107, 114, 128, 0.15)"
|
||||
icon="storage"
|
||||
delay={140}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isDebrid && (
|
||||
<StreamMetaBadge
|
||||
text="DEBRID"
|
||||
color="#00d4aa"
|
||||
bgColor="rgba(0, 212, 170, 0.15)"
|
||||
icon="flash-on"
|
||||
delay={160}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isHDRezka && (
|
||||
<StreamMetaBadge
|
||||
text="HDREZKA"
|
||||
color="#00d4aa"
|
||||
bgColor="rgba(0, 212, 170, 0.15)"
|
||||
icon="verified"
|
||||
delay={180}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Enhanced Action Icon */}
|
||||
<View style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: isSelected
|
||||
? 'rgba(229, 9, 20, 0.15)'
|
||||
: 'rgba(255, 255, 255, 0.05)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: isSelected
|
||||
? 'rgba(229, 9, 20, 0.3)'
|
||||
: 'rgba(255, 255, 255, 0.1)',
|
||||
elevation: 4,
|
||||
shadowColor: isSelected ? '#E50914' : '#fff',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: isSelected ? 0.2 : 0.05,
|
||||
shadowRadius: 4,
|
||||
}}>
|
||||
{isSelected ? (
|
||||
<Animated.View entering={ZoomIn.duration(200)}>
|
||||
<MaterialIcons name="check-circle" size={24} color="#E50914" />
|
||||
</Animated.View>
|
||||
) : (
|
||||
<MaterialIcons name="play-arrow" size={24} color="rgba(255,255,255,0.6)" />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{isChangingSource && isSelected && (
|
||||
<ActivityIndicator size="small" color="#E50914" style={{ marginLeft: 8 }} />
|
||||
)}
|
||||
</View>
|
||||
|
||||
{!isHDRezka && stream.title && stream.title !== stream.name && (
|
||||
<Text style={styles.sourceStreamSubtitle}>{stream.title}</Text>
|
||||
)}
|
||||
|
||||
<View style={styles.sourceStreamMeta}>
|
||||
{quality && quality >= "720" && (
|
||||
<QualityBadge type="HD" />
|
||||
)}
|
||||
|
||||
{isDolby && (
|
||||
<QualityBadge type="VISION" />
|
||||
)}
|
||||
|
||||
{size && (
|
||||
<View style={styles.sourceChip}>
|
||||
<Text style={styles.sourceChipText}>{size}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{isDebrid && (
|
||||
<View style={[styles.sourceChip, styles.debridChip]}>
|
||||
<Text style={styles.sourceChipText}>DEBRID</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{isHDRezka && (
|
||||
<View style={[styles.sourceChip, styles.hdrezkaChip]}>
|
||||
<Text style={styles.sourceChipText}>HDREZKA</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.sourceStreamAction}>
|
||||
{isSelected ? (
|
||||
<MaterialIcons name="check-circle" size={24} color="#E50914" />
|
||||
) : (
|
||||
<MaterialIcons name="play-arrow" size={24} color="rgba(255,255,255,0.7)" />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</Animated.View>
|
||||
))}
|
||||
</ScrollView>
|
||||
</BlurView>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue