Refactor AudioTrackModal and SourcesModal for improved UI and performance, including updated animations, streamlined layout, and removal of unused components.

This commit is contained in:
tapframe 2025-07-08 14:25:06 +05:30
parent f331d2becb
commit e94d04ae1b
3 changed files with 339 additions and 684 deletions

View file

@ -29,12 +29,12 @@ import {
} from './utils/playerTypes'; } from './utils/playerTypes';
import { safeDebugLog, parseSRT, DEBUG_MODE, formatTime } from './utils/playerUtils'; import { safeDebugLog, parseSRT, DEBUG_MODE, formatTime } from './utils/playerUtils';
import { styles } from './utils/playerStyles'; import { styles } from './utils/playerStyles';
import SubtitleModals from './modals/SubtitleModals'; import { SubtitleModals } from './modals/SubtitleModals';
import AudioTrackModal from './modals/AudioTrackModal'; import { AudioTrackModal } from './modals/AudioTrackModal';
import ResumeOverlay from './modals/ResumeOverlay'; import ResumeOverlay from './modals/ResumeOverlay';
import PlayerControls from './controls/PlayerControls'; import PlayerControls from './controls/PlayerControls';
import CustomSubtitles from './subtitles/CustomSubtitles'; import CustomSubtitles from './subtitles/CustomSubtitles';
import SourcesModal from './modals/SourcesModal'; import { SourcesModal } from './modals/SourcesModal';
// Map VLC resize modes to react-native-video resize modes // Map VLC resize modes to react-native-video resize modes
const getVideoResizeMode = (resizeMode: ResizeModeType) => { const getVideoResizeMode = (resizeMode: ResizeModeType) => {

View file

@ -1,16 +1,12 @@
import React from 'react'; import React from 'react';
import { View, Text, TouchableOpacity, ScrollView, Dimensions } from 'react-native'; import { View, Text, TouchableOpacity, ScrollView, Dimensions } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { BlurView } from 'expo-blur';
import Animated, { import Animated, {
FadeIn, FadeIn,
FadeOut, FadeOut,
useAnimatedStyle, SlideInRight,
useSharedValue, SlideOutRight,
withTiming,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { LinearGradient } from 'expo-linear-gradient';
import { styles } from '../utils/playerStyles';
import { getTrackDisplayName } from '../utils/playerUtils'; import { getTrackDisplayName } from '../utils/playerUtils';
interface AudioTrackModalProps { interface AudioTrackModalProps {
@ -21,52 +17,8 @@ interface AudioTrackModalProps {
selectAudioTrack: (trackId: number) => void; selectAudioTrack: (trackId: number) => void;
} }
const { width, height } = Dimensions.get('window'); const { width } = Dimensions.get('window');
const MENU_WIDTH = Math.min(width * 0.85, 400);
const MODAL_WIDTH = Math.min(width - 32, 520);
const MODAL_MAX_HEIGHT = height * 0.85;
const AudioBadge = ({
text,
color,
bgColor,
icon
}: {
text: string;
color: string;
bgColor: string;
icon?: string;
}) => (
<View
style={{
backgroundColor: bgColor,
borderColor: `${color}40`,
borderWidth: 1,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 8,
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={12} color={color} style={{ marginRight: 4 }} />
)}
<Text style={{
color: color,
fontSize: 10,
fontWeight: '700',
letterSpacing: 0.3,
}}>
{text}
</Text>
</View>
);
export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
showAudioModal, showAudioModal,
@ -75,325 +27,173 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
selectedAudioTrack, selectedAudioTrack,
selectAudioTrack, selectAudioTrack,
}) => { }) => {
const modalOpacity = useSharedValue(0);
React.useEffect(() => {
if (showAudioModal) {
modalOpacity.value = withTiming(1, { duration: 200 });
}
}, [showAudioModal]);
const modalStyle = useAnimatedStyle(() => ({
opacity: modalOpacity.value,
}));
const handleClose = () => { const handleClose = () => {
modalOpacity.value = withTiming(0, { duration: 150 }); setShowAudioModal(false);
setTimeout(() => setShowAudioModal(false), 150);
}; };
if (!showAudioModal) return null; if (!showAudioModal) return null;
return ( return (
<Animated.View <>
entering={FadeIn.duration(200)} {/* Backdrop */}
exiting={FadeOut.duration(150)} <Animated.View
style={{ entering={FadeIn.duration(200)}
position: 'absolute', exiting={FadeOut.duration(150)}
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.9)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999,
padding: 16,
}}
>
<TouchableOpacity
style={{ style={{
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 9998,
}} }}
onPress={handleClose}
activeOpacity={1}
/>
<Animated.View
style={[
{
width: MODAL_WIDTH,
maxHeight: MODAL_MAX_HEIGHT,
minHeight: height * 0.3,
overflow: 'hidden',
elevation: 25,
shadowColor: '#000',
shadowOffset: { width: 0, height: 12 },
shadowOpacity: 0.4,
shadowRadius: 25,
alignSelf: 'center',
},
modalStyle,
]}
> >
<BlurView <TouchableOpacity
intensity={100} style={{ flex: 1 }}
tint="dark" onPress={handleClose}
style={{ activeOpacity={1}
borderRadius: 28, />
overflow: 'hidden', </Animated.View>
backgroundColor: 'rgba(26, 26, 26, 0.8)',
width: '100%', {/* Side Menu */}
height: '100%', <Animated.View
}} entering={SlideInRight.duration(300)}
> exiting={SlideOutRight.duration(250)}
<LinearGradient style={{
colors={[ position: 'absolute',
'rgba(249, 115, 22, 0.95)', top: 0,
'rgba(234, 88, 12, 0.95)', right: 0,
'rgba(194, 65, 12, 0.9)' bottom: 0,
]} width: MENU_WIDTH,
locations={[0, 0.6, 1]} backgroundColor: '#1A1A1A',
zIndex: 9999,
elevation: 20,
shadowColor: '#000',
shadowOffset: { width: -5, height: 0 },
shadowOpacity: 0.3,
shadowRadius: 10,
}}
>
{/* Header */}
<View style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 50,
paddingBottom: 20,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
}}>
<Text style={{
color: '#FFFFFF',
fontSize: 22,
fontWeight: '700',
}}>
Audio Tracks
</Text>
<TouchableOpacity
style={{ style={{
paddingHorizontal: 28, width: 36,
paddingVertical: 24, height: 36,
flexDirection: 'row', borderRadius: 18,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between',
borderBottomWidth: 1,
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
width: '100%',
}} }}
onPress={handleClose}
activeOpacity={0.7}
> >
<View style={{ flex: 1 }}> <MaterialIcons name="close" size={20} color="#FFFFFF" />
<Text style={{ </TouchableOpacity>
color: '#fff', </View>
fontSize: 24,
fontWeight: '800',
letterSpacing: -0.8,
textShadowColor: 'rgba(0, 0, 0, 0.3)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 2,
}}>
Audio Tracks
</Text>
<Text style={{
color: 'rgba(255, 255, 255, 0.85)',
fontSize: 14,
marginTop: 4,
fontWeight: '500',
letterSpacing: 0.2,
}}>
Choose from {vlcAudioTracks.length} available track{vlcAudioTracks.length !== 1 ? 's' : ''}
</Text>
</View>
<TouchableOpacity <ScrollView
style={{ style={{ flex: 1 }}
width: 44, contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
height: 44, showsVerticalScrollIndicator={false}
borderRadius: 22, >
backgroundColor: 'rgba(255, 255, 255, 0.15)', {/* Audio Tracks */}
justifyContent: 'center', <View>
alignItems: 'center', <Text style={{
marginLeft: 16, color: 'rgba(255, 255, 255, 0.7)',
borderWidth: 1, fontSize: 14,
borderColor: 'rgba(255, 255, 255, 0.2)', fontWeight: '600',
}} marginBottom: 15,
onPress={handleClose} textTransform: 'uppercase',
activeOpacity={0.7} letterSpacing: 0.5,
> }}>
<MaterialIcons name="close" size={20} color="#fff" /> Available Tracks ({vlcAudioTracks.length})
</TouchableOpacity> </Text>
</LinearGradient>
<ScrollView <View style={{ gap: 8 }}>
style={{ {vlcAudioTracks.map((track) => {
maxHeight: MODAL_MAX_HEIGHT - 100, const isSelected = selectedAudioTrack === track.id;
backgroundColor: 'transparent', return (
width: '100%',
}}
showsVerticalScrollIndicator={false}
contentContainerStyle={{
padding: 24,
paddingBottom: 32,
width: '100%',
}}
bounces={false}
>
<View style={styles.modernTrackListContainer}>
{vlcAudioTracks.length > 0 ? vlcAudioTracks.map((track) => (
<View
key={track.id}
style={{
marginBottom: 16,
width: '100%',
}}
>
<TouchableOpacity <TouchableOpacity
key={track.id}
style={{ style={{
backgroundColor: selectedAudioTrack === track.id backgroundColor: isSelected ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255, 255, 255, 0.05)',
? 'rgba(249, 115, 22, 0.08)' borderRadius: 12,
: 'rgba(255, 255, 255, 0.03)', padding: 16,
borderRadius: 20, borderWidth: 1,
padding: 20, borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)',
borderWidth: 2,
borderColor: selectedAudioTrack === track.id
? 'rgba(249, 115, 22, 0.4)'
: 'rgba(255, 255, 255, 0.08)',
elevation: selectedAudioTrack === track.id ? 8 : 3,
shadowColor: selectedAudioTrack === track.id ? '#F97316' : '#000',
shadowOffset: { width: 0, height: selectedAudioTrack === track.id ? 4 : 2 },
shadowOpacity: selectedAudioTrack === track.id ? 0.3 : 0.1,
shadowRadius: selectedAudioTrack === track.id ? 12 : 6,
width: '100%',
}} }}
onPress={() => { onPress={() => {
selectAudioTrack(track.id); selectAudioTrack(track.id);
handleClose();
}} }}
activeOpacity={0.85} activeOpacity={0.7}
> >
<View style={{ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
flexDirection: 'row', <View style={{ flex: 1 }}>
alignItems: 'center', <Text style={{
justifyContent: 'space-between', color: '#FFFFFF',
width: '100%', fontSize: 15,
}}> fontWeight: '500',
<View style={{ flex: 1, marginRight: 16 }}> marginBottom: 4,
<View style={{
flexDirection: 'row',
alignItems: 'center',
marginBottom: 8,
gap: 12,
}}> }}>
{getTrackDisplayName(track)}
</Text>
{track.language && (
<Text style={{ <Text style={{
color: selectedAudioTrack === track.id ? '#fff' : 'rgba(255, 255, 255, 0.95)', color: 'rgba(255, 255, 255, 0.6)',
fontSize: 16, fontSize: 13,
fontWeight: '700',
letterSpacing: -0.2,
flex: 1,
}}> }}>
{getTrackDisplayName(track)} {track.language.toUpperCase()}
</Text> </Text>
)}
{selectedAudioTrack === track.id && (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(249, 115, 22, 0.25)',
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 14,
borderWidth: 1,
borderColor: 'rgba(249, 115, 22, 0.5)',
}}
>
<MaterialIcons name="volume-up" size={12} color="#F97316" />
<Text style={{
color: '#F97316',
fontSize: 10,
fontWeight: '800',
marginLeft: 3,
letterSpacing: 0.3,
}}>
ACTIVE
</Text>
</View>
)}
</View>
<View style={{
flexDirection: 'row',
flexWrap: 'wrap',
gap: 6,
alignItems: 'center',
}}>
<AudioBadge
text="AUDIO TRACK"
color="#F97316"
bgColor="rgba(249, 115, 22, 0.15)"
icon="audiotrack"
/>
{track.language && (
<AudioBadge
text={track.language.toUpperCase()}
color="#6B7280"
bgColor="rgba(107, 114, 128, 0.15)"
icon="language"
/>
)}
</View>
</View>
<View style={{
width: 48,
height: 48,
borderRadius: 24,
backgroundColor: selectedAudioTrack === track.id
? 'rgba(249, 115, 22, 0.15)'
: 'rgba(255, 255, 255, 0.05)',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 2,
borderColor: selectedAudioTrack === track.id
? 'rgba(249, 115, 22, 0.3)'
: 'rgba(255, 255, 255, 0.1)',
}}>
<MaterialIcons
name={selectedAudioTrack === track.id ? "check-circle" : "volume-up"}
size={24}
color={selectedAudioTrack === track.id ? "#F97316" : "rgba(255,255,255,0.6)"}
/>
</View> </View>
{isSelected && (
<MaterialIcons name="check" size={20} color="#22C55E" />
)}
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</View> );
)) : ( })}
<View
style={{
backgroundColor: 'rgba(255, 255, 255, 0.02)',
borderRadius: 20,
padding: 40,
alignItems: 'center',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.05)',
width: '100%',
}}
>
<MaterialIcons name="volume-off" size={48} color="rgba(255, 255, 255, 0.3)" />
<Text style={{
color: 'rgba(255, 255, 255, 0.6)',
fontSize: 18,
fontWeight: '700',
marginTop: 16,
textAlign: 'center',
letterSpacing: -0.3,
}}>
No audio tracks found
</Text>
<Text style={{
color: 'rgba(255, 255, 255, 0.4)',
fontSize: 14,
marginTop: 8,
textAlign: 'center',
lineHeight: 20,
}}>
No audio tracks are available for this content.{'\n'}Try a different source or check your connection.
</Text>
</View>
)}
</View> </View>
</ScrollView>
</BlurView> {vlcAudioTracks.length === 0 && (
<View style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: 12,
padding: 20,
alignItems: 'center',
}}>
<MaterialIcons name="volume-off" size={48} color="rgba(255,255,255,0.3)" />
<Text style={{
color: 'rgba(255, 255, 255, 0.6)',
fontSize: 16,
marginTop: 16,
textAlign: 'center',
}}>
No audio tracks available
</Text>
</View>
)}
</View>
</ScrollView>
</Animated.View> </Animated.View>
</Animated.View> </>
); );
}; };
export default AudioTrackModal;

View file

@ -1,19 +1,13 @@
import React from 'react'; import React from 'react';
import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Dimensions } from 'react-native'; import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Dimensions } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { BlurView } from 'expo-blur';
import Animated, { import Animated, {
FadeIn, FadeIn,
FadeOut, FadeOut,
useAnimatedStyle, SlideInRight,
useSharedValue, SlideOutRight,
withTiming,
runOnJS,
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { LinearGradient } from 'expo-linear-gradient';
import { styles } from '../utils/playerStyles';
import { Stream } from '../../../types/streams'; import { Stream } from '../../../types/streams';
import QualityBadge from '../../metadata/QualityBadge';
interface SourcesModalProps { interface SourcesModalProps {
showSourcesModal: boolean; showSourcesModal: boolean;
@ -24,12 +18,10 @@ interface SourcesModalProps {
isChangingSource: boolean; isChangingSource: boolean;
} }
const { width, height } = Dimensions.get('window'); const { width } = Dimensions.get('window');
const MENU_WIDTH = Math.min(width * 0.85, 400);
const MODAL_WIDTH = Math.min(width - 32, 520); const QualityBadge = ({ quality }: { quality: string | null }) => {
const MODAL_MAX_HEIGHT = height * 0.85;
const QualityIndicator = ({ quality }: { quality: string | null }) => {
if (!quality) return null; if (!quality) return null;
const qualityNum = parseInt(quality); const qualityNum = parseInt(quality);
@ -54,22 +46,15 @@ const QualityIndicator = ({ quality }: { quality: string | null }) => {
borderColor: `${color}60`, borderColor: `${color}60`,
borderWidth: 1, borderWidth: 1,
paddingHorizontal: 8, paddingHorizontal: 8,
paddingVertical: 3, paddingVertical: 4,
borderRadius: 8, borderRadius: 8,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
}} }}
> >
<View style={{
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: color,
marginRight: 4,
}} />
<Text style={{ <Text style={{
color: color, color: color,
fontSize: 10, fontSize: 12,
fontWeight: '700', fontWeight: '700',
letterSpacing: 0.5, letterSpacing: 0.5,
}}> }}>
@ -79,49 +64,7 @@ const QualityIndicator = ({ quality }: { quality: string | null }) => {
); );
}; };
const StreamMetaBadge = ({ export const SourcesModal: React.FC<SourcesModalProps> = ({
text,
color,
bgColor,
icon
}: {
text: string;
color: string;
bgColor: string;
icon?: string;
}) => (
<View
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>
</View>
);
const SourcesModal: React.FC<SourcesModalProps> = ({
showSourcesModal, showSourcesModal,
setShowSourcesModal, setShowSourcesModal,
availableStreams, availableStreams,
@ -129,28 +72,8 @@ const SourcesModal: React.FC<SourcesModalProps> = ({
onSelectStream, onSelectStream,
isChangingSource, isChangingSource,
}) => { }) => {
const modalOpacity = useSharedValue(0);
React.useEffect(() => {
if (showSourcesModal) {
modalOpacity.value = withTiming(1, { duration: 200 });
} else {
modalOpacity.value = withTiming(0, { duration: 150 });
}
return () => {
modalOpacity.value = 0;
};
}, [showSourcesModal]);
const modalStyle = useAnimatedStyle(() => ({
opacity: modalOpacity.value,
}));
const handleClose = () => { const handleClose = () => {
modalOpacity.value = withTiming(0, { duration: 150 }, () => { setShowSourcesModal(false);
runOnJS(setShowSourcesModal)(false);
});
}; };
if (!showSourcesModal) return null; if (!showSourcesModal) return null;
@ -174,305 +97,237 @@ const SourcesModal: React.FC<SourcesModalProps> = ({
}; };
return ( return (
<Animated.View <>
entering={FadeIn.duration(200)} {/* Backdrop */}
exiting={FadeOut.duration(150)} <Animated.View
style={{ entering={FadeIn.duration(200)}
position: 'absolute', exiting={FadeOut.duration(150)}
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.9)',
justifyContent: 'center',
alignItems: 'center',
zIndex: 9999,
padding: 16,
}}
>
<TouchableOpacity
style={{ style={{
position: 'absolute', position: 'absolute',
top: 0, top: 0,
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
zIndex: 9998,
}} }}
onPress={handleClose}
activeOpacity={1}
/>
<Animated.View
style={[
{
width: MODAL_WIDTH,
maxHeight: MODAL_MAX_HEIGHT,
minHeight: height * 0.3,
overflow: 'hidden',
elevation: 25,
shadowColor: '#000',
shadowOffset: { width: 0, height: 12 },
shadowOpacity: 0.4,
shadowRadius: 25,
alignSelf: 'center',
},
modalStyle,
]}
> >
<BlurView <TouchableOpacity
intensity={100} style={{ flex: 1 }}
tint="dark" onPress={handleClose}
style={{ activeOpacity={1}
borderRadius: 28, />
overflow: 'hidden', </Animated.View>
backgroundColor: 'rgba(26, 26, 26, 0.8)',
width: '100%', {/* Side Menu */}
height: '100%', <Animated.View
}} entering={SlideInRight.duration(300)}
> exiting={SlideOutRight.duration(250)}
<LinearGradient style={{
colors={[ position: 'absolute',
'rgba(249, 115, 22, 0.95)', top: 0,
'rgba(234, 88, 12, 0.95)', right: 0,
'rgba(194, 65, 12, 0.9)' bottom: 0,
]} width: MENU_WIDTH,
locations={[0, 0.6, 1]} backgroundColor: '#1A1A1A',
zIndex: 9999,
elevation: 20,
shadowColor: '#000',
shadowOffset: { width: -5, height: 0 },
shadowOpacity: 0.3,
shadowRadius: 10,
}}
>
{/* Header */}
<View style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 50,
paddingBottom: 20,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
}}>
<Text style={{
color: '#FFFFFF',
fontSize: 22,
fontWeight: '700',
}}>
Change Source
</Text>
<TouchableOpacity
style={{ style={{
paddingHorizontal: 28, width: 36,
paddingVertical: 24, height: 36,
borderRadius: 18,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
justifyContent: 'center',
alignItems: 'center',
}}
onPress={handleClose}
activeOpacity={0.7}
>
<MaterialIcons name="close" size={20} color="#FFFFFF" />
</TouchableOpacity>
</View>
<ScrollView
style={{ flex: 1 }}
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
showsVerticalScrollIndicator={false}
>
{isChangingSource && (
<View style={{
backgroundColor: 'rgba(34, 197, 94, 0.1)',
borderRadius: 12,
padding: 16,
marginBottom: 20,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', }}>
borderBottomWidth: 1, <ActivityIndicator size="small" color="#22C55E" />
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
width: '100%',
}}
>
<View style={{ flex: 1 }}>
<Text style={{ <Text style={{
color: '#fff', color: '#22C55E',
fontSize: 24,
fontWeight: '800',
letterSpacing: -0.8,
textShadowColor: 'rgba(0, 0, 0, 0.3)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 2,
}}>
Video Sources
</Text>
<Text style={{
color: 'rgba(255, 255, 255, 0.85)',
fontSize: 14, fontSize: 14,
marginTop: 4, fontWeight: '600',
fontWeight: '500', marginLeft: 12,
letterSpacing: 0.2,
}}> }}>
Choose from {Object.values(availableStreams).reduce((acc, curr) => acc + curr.streams.length, 0)} available sources Switching source...
</Text> </Text>
</View> </View>
)}
<TouchableOpacity {sortedProviders.length > 0 ? (
style={{ sortedProviders.map(([providerId, providerData]) => (
width: 44, <View key={providerId} style={{ marginBottom: 30 }}>
height: 44, <Text style={{
borderRadius: 22, color: 'rgba(255, 255, 255, 0.7)',
backgroundColor: 'rgba(255, 255, 255, 0.15)', fontSize: 14,
justifyContent: 'center', fontWeight: '600',
alignItems: 'center', marginBottom: 15,
marginLeft: 16, textTransform: 'uppercase',
borderWidth: 1, letterSpacing: 0.5,
borderColor: 'rgba(255, 255, 255, 0.2)',
}}
onPress={handleClose}
activeOpacity={0.7}
>
<MaterialIcons name="close" size={20} color="#fff" />
</TouchableOpacity>
</LinearGradient>
<ScrollView
style={{
maxHeight: MODAL_MAX_HEIGHT - 100,
backgroundColor: 'transparent',
width: '100%',
}}
showsVerticalScrollIndicator={false}
contentContainerStyle={{
padding: 24,
paddingBottom: 32,
width: '100%',
}}
bounces={false}
>
{sortedProviders.map(([providerId, { streams, addonName }]) => (
<View key={providerId} style={{ marginBottom: 24 }}>
<View style={{
flexDirection: 'row',
alignItems: 'center',
marginBottom: 12,
}}> }}>
<Text style={{ {providerData.addonName} ({providerData.streams.length})
color: 'rgba(255, 255, 255, 0.7)', </Text>
fontSize: 14,
fontWeight: '600',
letterSpacing: 0.3,
textTransform: 'uppercase',
}}>
{addonName}
</Text>
<View style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
paddingHorizontal: 8,
paddingVertical: 2,
borderRadius: 12,
marginLeft: 8,
}}>
<Text style={{
color: 'rgba(255, 255, 255, 0.5)',
fontSize: 12,
fontWeight: '600',
}}>
{streams.length}
</Text>
</View>
</View>
{streams.map((stream, index) => { <View style={{ gap: 8 }}>
const isSelected = isStreamSelected(stream); {providerData.streams.map((stream, index) => {
const quality = getQualityFromTitle(stream.title); const isSelected = isStreamSelected(stream);
const quality = getQualityFromTitle(stream.title) || stream.quality;
return ( return (
<View
key={`${stream.url}-${index}`}
style={{
marginBottom: 12,
width: '100%',
}}
>
<TouchableOpacity <TouchableOpacity
key={`${providerId}-${index}`}
style={{ style={{
backgroundColor: isSelected backgroundColor: isSelected ? 'rgba(59, 130, 246, 0.15)' : 'rgba(255, 255, 255, 0.05)',
? 'rgba(249, 115, 22, 0.08)' borderRadius: 12,
: 'rgba(255, 255, 255, 0.03)', padding: 16,
borderRadius: 20, borderWidth: 1,
padding: 20, borderColor: isSelected ? 'rgba(59, 130, 246, 0.3)' : 'rgba(255, 255, 255, 0.1)',
borderWidth: 2, opacity: isChangingSource && !isSelected ? 0.6 : 1,
borderColor: isSelected
? 'rgba(249, 115, 22, 0.4)'
: 'rgba(255, 255, 255, 0.08)',
elevation: isSelected ? 8 : 3,
shadowColor: isSelected ? '#F97316' : '#000',
shadowOffset: { width: 0, height: isSelected ? 4 : 2 },
shadowOpacity: isSelected ? 0.3 : 0.1,
shadowRadius: isSelected ? 12 : 6,
width: '100%',
}} }}
onPress={() => handleStreamSelect(stream)} onPress={() => handleStreamSelect(stream)}
activeOpacity={0.85} activeOpacity={0.7}
disabled={isChangingSource} disabled={isChangingSource}
> >
<View style={{ <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
flexDirection: 'row', <View style={{ flex: 1 }}>
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
}}>
<View style={{ flex: 1, marginRight: 16 }}>
<View style={{ <View style={{
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
marginBottom: 8, marginBottom: 8,
gap: 12, gap: 8,
}}> }}>
<Text style={{ <Text style={{
color: isSelected ? '#fff' : 'rgba(255, 255, 255, 0.95)', color: '#FFFFFF',
fontSize: 16, fontSize: 15,
fontWeight: '700', fontWeight: '500',
letterSpacing: -0.2,
flex: 1, flex: 1,
}}> }}>
{stream.title || 'Untitled Stream'} {stream.title || stream.name || `Stream ${index + 1}`}
</Text> </Text>
{quality && <QualityBadge quality={quality} />}
{isSelected && (
<View
style={{
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(249, 115, 22, 0.25)',
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 14,
borderWidth: 1,
borderColor: 'rgba(249, 115, 22, 0.5)',
}}
>
<MaterialIcons name="play-arrow" size={12} color="#F97316" />
<Text style={{
color: '#F97316',
fontSize: 10,
fontWeight: '800',
marginLeft: 3,
letterSpacing: 0.3,
}}>
PLAYING
</Text>
</View>
)}
</View> </View>
<View style={{ {(stream.size || stream.lang) && (
flexDirection: 'row', <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
flexWrap: 'wrap', {stream.size && (
gap: 6, <View style={{ flexDirection: 'row', alignItems: 'center' }}>
alignItems: 'center', <MaterialIcons name="storage" size={14} color="rgba(107, 114, 128, 0.8)" />
}}> <Text style={{
{quality && <QualityIndicator quality={quality} />} color: 'rgba(107, 114, 128, 0.8)',
<StreamMetaBadge fontSize: 12,
text={providerId.toUpperCase()} fontWeight: '600',
color="#6B7280" marginLeft: 4,
bgColor="rgba(107, 114, 128, 0.15)" }}>
icon="source" {(stream.size / (1024 * 1024 * 1024)).toFixed(1)} GB
/> </Text>
</View> </View>
)}
{stream.lang && (
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialIcons name="language" size={14} color="rgba(59, 130, 246, 0.8)" />
<Text style={{
color: 'rgba(59, 130, 246, 0.8)',
fontSize: 12,
fontWeight: '600',
marginLeft: 4,
}}>
{stream.lang.toUpperCase()}
</Text>
</View>
)}
</View>
)}
</View> </View>
<View style={{ <View style={{
width: 48, marginLeft: 12,
height: 48,
borderRadius: 24,
backgroundColor: isSelected
? 'rgba(249, 115, 22, 0.15)'
: 'rgba(255, 255, 255, 0.05)',
justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
borderWidth: 2,
borderColor: isSelected
? 'rgba(249, 115, 22, 0.3)'
: 'rgba(255, 255, 255, 0.1)',
}}> }}>
{isChangingSource ? ( {isSelected ? (
<ActivityIndicator size="small" color="#F97316" /> <MaterialIcons name="check" size={20} color="#3B82F6" />
) : ( ) : (
<MaterialIcons <MaterialIcons name="play-arrow" size={20} color="rgba(255,255,255,0.4)" />
name={isSelected ? "check-circle" : "play-circle-outline"}
size={24}
color={isSelected ? "#F97316" : "rgba(255,255,255,0.6)"}
/>
)} )}
</View> </View>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
</View> );
); })}
})} </View>
</View> </View>
))} ))
</ScrollView> ) : (
</BlurView> <View style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: 12,
padding: 20,
alignItems: 'center',
}}>
<MaterialIcons name="error-outline" size={48} color="rgba(255,255,255,0.3)" />
<Text style={{
color: 'rgba(255, 255, 255, 0.6)',
fontSize: 16,
marginTop: 16,
textAlign: 'center',
}}>
No sources available
</Text>
<Text style={{
color: 'rgba(255, 255, 255, 0.4)',
fontSize: 14,
marginTop: 8,
textAlign: 'center',
}}>
Try searching for different content
</Text>
</View>
)}
</ScrollView>
</Animated.View> </Animated.View>
</Animated.View> </>
); );
}; };
export default SourcesModal;