mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
Refactor AudioTrackModal and SourcesModal for improved UI and performance, including updated animations, streamlined layout, and removal of unused components.
This commit is contained in:
parent
f331d2becb
commit
e94d04ae1b
3 changed files with 339 additions and 684 deletions
|
|
@ -29,12 +29,12 @@ import {
|
|||
} from './utils/playerTypes';
|
||||
import { safeDebugLog, parseSRT, DEBUG_MODE, formatTime } from './utils/playerUtils';
|
||||
import { styles } from './utils/playerStyles';
|
||||
import SubtitleModals from './modals/SubtitleModals';
|
||||
import AudioTrackModal from './modals/AudioTrackModal';
|
||||
import { SubtitleModals } from './modals/SubtitleModals';
|
||||
import { AudioTrackModal } from './modals/AudioTrackModal';
|
||||
import ResumeOverlay from './modals/ResumeOverlay';
|
||||
import PlayerControls from './controls/PlayerControls';
|
||||
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
|
||||
const getVideoResizeMode = (resizeMode: ResizeModeType) => {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView, Dimensions } from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
SlideInRight,
|
||||
SlideOutRight,
|
||||
} from 'react-native-reanimated';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { styles } from '../utils/playerStyles';
|
||||
import { getTrackDisplayName } from '../utils/playerUtils';
|
||||
|
||||
interface AudioTrackModalProps {
|
||||
|
|
@ -21,52 +17,8 @@ interface AudioTrackModalProps {
|
|||
selectAudioTrack: (trackId: number) => void;
|
||||
}
|
||||
|
||||
const { width, height } = Dimensions.get('window');
|
||||
|
||||
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>
|
||||
);
|
||||
const { width } = Dimensions.get('window');
|
||||
const MENU_WIDTH = Math.min(width * 0.85, 400);
|
||||
|
||||
export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
||||
showAudioModal,
|
||||
|
|
@ -75,325 +27,173 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
|||
selectedAudioTrack,
|
||||
selectAudioTrack,
|
||||
}) => {
|
||||
const modalOpacity = useSharedValue(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (showAudioModal) {
|
||||
modalOpacity.value = withTiming(1, { duration: 200 });
|
||||
}
|
||||
}, [showAudioModal]);
|
||||
|
||||
const modalStyle = useAnimatedStyle(() => ({
|
||||
opacity: modalOpacity.value,
|
||||
}));
|
||||
|
||||
const handleClose = () => {
|
||||
modalOpacity.value = withTiming(0, { duration: 150 });
|
||||
setTimeout(() => setShowAudioModal(false), 150);
|
||||
setShowAudioModal(false);
|
||||
};
|
||||
|
||||
if (!showAudioModal) return null;
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(200)}
|
||||
exiting={FadeOut.duration(150)}
|
||||
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,
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(200)}
|
||||
exiting={FadeOut.duration(150)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 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
|
||||
intensity={100}
|
||||
tint="dark"
|
||||
style={{
|
||||
borderRadius: 28,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'rgba(26, 26, 26, 0.8)',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[
|
||||
'rgba(249, 115, 22, 0.95)',
|
||||
'rgba(234, 88, 12, 0.95)',
|
||||
'rgba(194, 65, 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)',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<View 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,
|
||||
}}>
|
||||
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
|
||||
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>
|
||||
</LinearGradient>
|
||||
<TouchableOpacity
|
||||
style={{ flex: 1 }}
|
||||
onPress={handleClose}
|
||||
activeOpacity={1}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
<ScrollView
|
||||
style={{
|
||||
maxHeight: MODAL_MAX_HEIGHT - 100,
|
||||
backgroundColor: 'transparent',
|
||||
width: '100%',
|
||||
{/* Side Menu */}
|
||||
<Animated.View
|
||||
entering={SlideInRight.duration(300)}
|
||||
exiting={SlideOutRight.duration(250)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: MENU_WIDTH,
|
||||
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={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={{
|
||||
padding: 24,
|
||||
paddingBottom: 32,
|
||||
width: '100%',
|
||||
}}
|
||||
bounces={false}
|
||||
onPress={handleClose}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.modernTrackListContainer}>
|
||||
{vlcAudioTracks.length > 0 ? vlcAudioTracks.map((track) => (
|
||||
<View
|
||||
key={track.id}
|
||||
style={{
|
||||
marginBottom: 16,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="close" size={20} color="#FFFFFF" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={{ flex: 1 }}
|
||||
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{/* Audio Tracks */}
|
||||
<View>
|
||||
<Text style={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginBottom: 15,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
}}>
|
||||
Available Tracks ({vlcAudioTracks.length})
|
||||
</Text>
|
||||
|
||||
<View style={{ gap: 8 }}>
|
||||
{vlcAudioTracks.map((track) => {
|
||||
const isSelected = selectedAudioTrack === track.id;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={track.id}
|
||||
style={{
|
||||
backgroundColor: selectedAudioTrack === track.id
|
||||
? 'rgba(249, 115, 22, 0.08)'
|
||||
: 'rgba(255, 255, 255, 0.03)',
|
||||
borderRadius: 20,
|
||||
padding: 20,
|
||||
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%',
|
||||
backgroundColor: isSelected ? 'rgba(34, 197, 94, 0.15)' : 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: isSelected ? 'rgba(34, 197, 94, 0.3)' : 'rgba(255, 255, 255, 0.1)',
|
||||
}}
|
||||
onPress={() => {
|
||||
selectAudioTrack(track.id);
|
||||
handleClose();
|
||||
}}
|
||||
activeOpacity={0.85}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
}}>
|
||||
<View style={{ flex: 1, marginRight: 16 }}>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
gap: 12,
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text style={{
|
||||
color: '#FFFFFF',
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
marginBottom: 4,
|
||||
}}>
|
||||
{getTrackDisplayName(track)}
|
||||
</Text>
|
||||
{track.language && (
|
||||
<Text style={{
|
||||
color: selectedAudioTrack === track.id ? '#fff' : 'rgba(255, 255, 255, 0.95)',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
letterSpacing: -0.2,
|
||||
flex: 1,
|
||||
color: 'rgba(255, 255, 255, 0.6)',
|
||||
fontSize: 13,
|
||||
}}>
|
||||
{getTrackDisplayName(track)}
|
||||
{track.language.toUpperCase()}
|
||||
</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>
|
||||
{isSelected && (
|
||||
<MaterialIcons name="check" size={20} color="#22C55E" />
|
||||
)}
|
||||
</View>
|
||||
</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>
|
||||
</ScrollView>
|
||||
</BlurView>
|
||||
</Animated.View>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
export default AudioTrackModal;
|
||||
{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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,19 +1,13 @@
|
|||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Dimensions } from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
runOnJS,
|
||||
SlideInRight,
|
||||
SlideOutRight,
|
||||
} 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';
|
||||
|
||||
interface SourcesModalProps {
|
||||
showSourcesModal: boolean;
|
||||
|
|
@ -24,12 +18,10 @@ interface SourcesModalProps {
|
|||
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 MODAL_MAX_HEIGHT = height * 0.85;
|
||||
|
||||
const QualityIndicator = ({ quality }: { quality: string | null }) => {
|
||||
const QualityBadge = ({ quality }: { quality: string | null }) => {
|
||||
if (!quality) return null;
|
||||
|
||||
const qualityNum = parseInt(quality);
|
||||
|
|
@ -54,22 +46,15 @@ const QualityIndicator = ({ quality }: { quality: string | null }) => {
|
|||
borderColor: `${color}60`,
|
||||
borderWidth: 1,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 8,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<View style={{
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: color,
|
||||
marginRight: 4,
|
||||
}} />
|
||||
<Text style={{
|
||||
color: color,
|
||||
fontSize: 10,
|
||||
fontSize: 12,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.5,
|
||||
}}>
|
||||
|
|
@ -79,49 +64,7 @@ const QualityIndicator = ({ quality }: { quality: string | null }) => {
|
|||
);
|
||||
};
|
||||
|
||||
const StreamMetaBadge = ({
|
||||
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> = ({
|
||||
export const SourcesModal: React.FC<SourcesModalProps> = ({
|
||||
showSourcesModal,
|
||||
setShowSourcesModal,
|
||||
availableStreams,
|
||||
|
|
@ -129,28 +72,8 @@ const SourcesModal: React.FC<SourcesModalProps> = ({
|
|||
onSelectStream,
|
||||
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 = () => {
|
||||
modalOpacity.value = withTiming(0, { duration: 150 }, () => {
|
||||
runOnJS(setShowSourcesModal)(false);
|
||||
});
|
||||
setShowSourcesModal(false);
|
||||
};
|
||||
|
||||
if (!showSourcesModal) return null;
|
||||
|
|
@ -174,305 +97,237 @@ const SourcesModal: React.FC<SourcesModalProps> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(200)}
|
||||
exiting={FadeOut.duration(150)}
|
||||
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,
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(200)}
|
||||
exiting={FadeOut.duration(150)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 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
|
||||
intensity={100}
|
||||
tint="dark"
|
||||
style={{
|
||||
borderRadius: 28,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'rgba(26, 26, 26, 0.8)',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[
|
||||
'rgba(249, 115, 22, 0.95)',
|
||||
'rgba(234, 88, 12, 0.95)',
|
||||
'rgba(194, 65, 12, 0.9)'
|
||||
]}
|
||||
locations={[0, 0.6, 1]}
|
||||
<TouchableOpacity
|
||||
style={{ flex: 1 }}
|
||||
onPress={handleClose}
|
||||
activeOpacity={1}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
{/* Side Menu */}
|
||||
<Animated.View
|
||||
entering={SlideInRight.duration(300)}
|
||||
exiting={SlideOutRight.duration(250)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
width: MENU_WIDTH,
|
||||
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={{
|
||||
paddingHorizontal: 28,
|
||||
paddingVertical: 24,
|
||||
width: 36,
|
||||
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',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: 'rgba(255, 255, 255, 0.1)',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
}}>
|
||||
<ActivityIndicator size="small" color="#22C55E" />
|
||||
<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,
|
||||
}}>
|
||||
Video Sources
|
||||
</Text>
|
||||
<Text style={{
|
||||
color: 'rgba(255, 255, 255, 0.85)',
|
||||
color: '#22C55E',
|
||||
fontSize: 14,
|
||||
marginTop: 4,
|
||||
fontWeight: '500',
|
||||
letterSpacing: 0.2,
|
||||
fontWeight: '600',
|
||||
marginLeft: 12,
|
||||
}}>
|
||||
Choose from {Object.values(availableStreams).reduce((acc, curr) => acc + curr.streams.length, 0)} available sources
|
||||
Switching source...
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<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>
|
||||
</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,
|
||||
{sortedProviders.length > 0 ? (
|
||||
sortedProviders.map(([providerId, providerData]) => (
|
||||
<View key={providerId} style={{ marginBottom: 30 }}>
|
||||
<Text style={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginBottom: 15,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 0.5,
|
||||
}}>
|
||||
<Text style={{
|
||||
color: 'rgba(255, 255, 255, 0.7)',
|
||||
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) => {
|
||||
const isSelected = isStreamSelected(stream);
|
||||
const quality = getQualityFromTitle(stream.title);
|
||||
|
||||
return (
|
||||
<View
|
||||
key={`${stream.url}-${index}`}
|
||||
style={{
|
||||
marginBottom: 12,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{providerData.addonName} ({providerData.streams.length})
|
||||
</Text>
|
||||
|
||||
<View style={{ gap: 8 }}>
|
||||
{providerData.streams.map((stream, index) => {
|
||||
const isSelected = isStreamSelected(stream);
|
||||
const quality = getQualityFromTitle(stream.title) || stream.quality;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={`${providerId}-${index}`}
|
||||
style={{
|
||||
backgroundColor: isSelected
|
||||
? 'rgba(249, 115, 22, 0.08)'
|
||||
: 'rgba(255, 255, 255, 0.03)',
|
||||
borderRadius: 20,
|
||||
padding: 20,
|
||||
borderWidth: 2,
|
||||
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%',
|
||||
backgroundColor: isSelected ? 'rgba(59, 130, 246, 0.15)' : 'rgba(255, 255, 255, 0.05)',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
borderWidth: 1,
|
||||
borderColor: isSelected ? 'rgba(59, 130, 246, 0.3)' : 'rgba(255, 255, 255, 0.1)',
|
||||
opacity: isChangingSource && !isSelected ? 0.6 : 1,
|
||||
}}
|
||||
onPress={() => handleStreamSelect(stream)}
|
||||
activeOpacity={0.85}
|
||||
activeOpacity={0.7}
|
||||
disabled={isChangingSource}
|
||||
>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
}}>
|
||||
<View style={{ flex: 1, marginRight: 16 }}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
gap: 12,
|
||||
gap: 8,
|
||||
}}>
|
||||
<Text style={{
|
||||
color: isSelected ? '#fff' : 'rgba(255, 255, 255, 0.95)',
|
||||
fontSize: 16,
|
||||
fontWeight: '700',
|
||||
letterSpacing: -0.2,
|
||||
color: '#FFFFFF',
|
||||
fontSize: 15,
|
||||
fontWeight: '500',
|
||||
flex: 1,
|
||||
}}>
|
||||
{stream.title || 'Untitled Stream'}
|
||||
{stream.title || stream.name || `Stream ${index + 1}`}
|
||||
</Text>
|
||||
|
||||
{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>
|
||||
)}
|
||||
{quality && <QualityBadge quality={quality} />}
|
||||
</View>
|
||||
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 6,
|
||||
alignItems: 'center',
|
||||
}}>
|
||||
{quality && <QualityIndicator quality={quality} />}
|
||||
<StreamMetaBadge
|
||||
text={providerId.toUpperCase()}
|
||||
color="#6B7280"
|
||||
bgColor="rgba(107, 114, 128, 0.15)"
|
||||
icon="source"
|
||||
/>
|
||||
</View>
|
||||
{(stream.size || stream.lang) && (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12 }}>
|
||||
{stream.size && (
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<MaterialIcons name="storage" size={14} color="rgba(107, 114, 128, 0.8)" />
|
||||
<Text style={{
|
||||
color: 'rgba(107, 114, 128, 0.8)',
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
marginLeft: 4,
|
||||
}}>
|
||||
{(stream.size / (1024 * 1024 * 1024)).toFixed(1)} GB
|
||||
</Text>
|
||||
</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 style={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: isSelected
|
||||
? 'rgba(249, 115, 22, 0.15)'
|
||||
: 'rgba(255, 255, 255, 0.05)',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 12,
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: isSelected
|
||||
? 'rgba(249, 115, 22, 0.3)'
|
||||
: 'rgba(255, 255, 255, 0.1)',
|
||||
}}>
|
||||
{isChangingSource ? (
|
||||
<ActivityIndicator size="small" color="#F97316" />
|
||||
{isSelected ? (
|
||||
<MaterialIcons name="check" size={20} color="#3B82F6" />
|
||||
) : (
|
||||
<MaterialIcons
|
||||
name={isSelected ? "check-circle" : "play-circle-outline"}
|
||||
size={24}
|
||||
color={isSelected ? "#F97316" : "rgba(255,255,255,0.6)"}
|
||||
/>
|
||||
<MaterialIcons name="play-arrow" size={20} color="rgba(255,255,255,0.4)" />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SourcesModal;
|
||||
};
|
||||
Loading…
Reference in a new issue