diff --git a/src/components/player/modals/AudioTrackModal.tsx b/src/components/player/modals/AudioTrackModal.tsx index c2363563..77e3bbba 100644 --- a/src/components/player/modals/AudioTrackModal.tsx +++ b/src/components/player/modals/AudioTrackModal.tsx @@ -1,24 +1,13 @@ import React from 'react'; import { View, Text, TouchableOpacity, ScrollView, Dimensions } from 'react-native'; -import { Ionicons, MaterialIcons } from '@expo/vector-icons'; +import { MaterialIcons } from '@expo/vector-icons'; import { BlurView } from 'expo-blur'; import Animated, { FadeIn, - FadeOut, - SlideInDown, - SlideOutDown, - FadeInDown, - FadeInUp, - Layout, - withSpring, - withTiming, + FadeOut, useAnimatedStyle, useSharedValue, - interpolate, - Easing, - withDelay, - withSequence, - runOnJS, + withTiming, } from 'react-native-reanimated'; import { LinearGradient } from 'expo-linear-gradient'; import { styles } from '../utils/playerStyles'; @@ -34,7 +23,6 @@ interface AudioTrackModalProps { const { width, height } = Dimensions.get('window'); -// Fixed dimensions for the modal const MODAL_WIDTH = Math.min(width - 32, 520); const MODAL_MAX_HEIGHT = height * 0.85; @@ -42,17 +30,14 @@ const AudioBadge = ({ text, color, bgColor, - icon, - delay = 0 + icon }: { text: string; color: string; bgColor: string; icon?: string; - delay?: number; }) => ( - {text} - + ); export const AudioTrackModal: React.FC = ({ @@ -90,30 +75,19 @@ export const AudioTrackModal: React.FC = ({ selectedAudioTrack, selectAudioTrack, }) => { - const modalScale = useSharedValue(0.9); const modalOpacity = useSharedValue(0); React.useEffect(() => { if (showAudioModal) { - modalScale.value = withSpring(1, { - damping: 20, - stiffness: 300, - mass: 0.8, - }); - modalOpacity.value = withTiming(1, { - duration: 200, - easing: Easing.out(Easing.quad), - }); + modalOpacity.value = withTiming(1, { duration: 200 }); } }, [showAudioModal]); const modalStyle = useAnimatedStyle(() => ({ - transform: [{ scale: modalScale.value }], opacity: modalOpacity.value, })); const handleClose = () => { - modalScale.value = withTiming(0.9, { duration: 150 }); modalOpacity.value = withTiming(0, { duration: 150 }); setTimeout(() => setShowAudioModal(false), 150); }; @@ -122,8 +96,8 @@ export const AudioTrackModal: React.FC = ({ return ( = ({ padding: 16, }} > - {/* Backdrop */} = ({ activeOpacity={1} /> - {/* Modal Content */} = ({ modalStyle, ]} > - {/* Glassmorphism Background */} = ({ height: '100%', }} > - {/* Header */} = ({ width: '100%', }} > - + = ({ }}> Choose from {vlcAudioTracks.length} available track{vlcAudioTracks.length !== 1 ? 's' : ''} - + - - - - - + + + - {/* Content */} = ({ bounces={false} > - {vlcAudioTracks.length > 0 ? vlcAudioTracks.map((track, index) => ( - 0 ? vlcAudioTracks.map((track) => ( + = ({ {selectedAudioTrack === track.id && ( - = ({ }}> ACTIVE - + )} @@ -365,7 +326,6 @@ export const AudioTrackModal: React.FC = ({ color="#6B7280" bgColor="rgba(107, 114, 128, 0.15)" icon="language" - delay={50} /> )} @@ -385,20 +345,17 @@ export const AudioTrackModal: React.FC = ({ ? 'rgba(249, 115, 22, 0.3)' : 'rgba(255, 255, 255, 0.1)', }}> - {selectedAudioTrack === track.id ? ( - - - - ) : ( - - )} + - + )) : ( - = ({ }}> No audio tracks are available for this content.{'\n'}Try a different source or check your connection. - + )} diff --git a/src/components/player/modals/SourcesModal.tsx b/src/components/player/modals/SourcesModal.tsx index ed2ad5ea..cd11cb4a 100644 --- a/src/components/player/modals/SourcesModal.tsx +++ b/src/components/player/modals/SourcesModal.tsx @@ -1,23 +1,13 @@ import React from 'react'; import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Dimensions } from 'react-native'; -import { Ionicons, MaterialIcons } from '@expo/vector-icons'; +import { MaterialIcons } from '@expo/vector-icons'; import { BlurView } from 'expo-blur'; import Animated, { FadeIn, - FadeOut, - SlideInDown, - SlideOutDown, - FadeInDown, - FadeInUp, - Layout, - withSpring, - withTiming, + FadeOut, useAnimatedStyle, useSharedValue, - interpolate, - Easing, - withDelay, - withSequence, + withTiming, runOnJS, } from 'react-native-reanimated'; import { LinearGradient } from 'expo-linear-gradient'; @@ -36,7 +26,6 @@ interface SourcesModalProps { const { width, height } = Dimensions.get('window'); -// Fixed dimensions for the modal const MODAL_WIDTH = Math.min(width - 32, 520); const MODAL_MAX_HEIGHT = height * 0.85; @@ -59,8 +48,7 @@ const QualityIndicator = ({ quality }: { quality: string | null }) => { } return ( - { }}> {label} - + ); }; @@ -95,17 +83,14 @@ const StreamMetaBadge = ({ text, color, bgColor, - icon, - delay = 0 + icon }: { text: string; color: string; bgColor: string; icon?: string; - delay?: number; }) => ( - {text} - + ); const SourcesModal: React.FC = ({ @@ -144,32 +129,33 @@ 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), - }); + modalOpacity.value = withTiming(1, { duration: 200 }); + } else { + modalOpacity.value = withTiming(0, { duration: 150 }); } + + return () => { + modalOpacity.value = 0; + }; }, [showSourcesModal]); const modalStyle = useAnimatedStyle(() => ({ - transform: [{ scale: modalScale.value }], opacity: modalOpacity.value, })); + const handleClose = () => { + modalOpacity.value = withTiming(0, { duration: 150 }, () => { + runOnJS(setShowSourcesModal)(false); + }); + }; + if (!showSourcesModal) return null; const sortedProviders = Object.entries(availableStreams).sort(([a], [b]) => { - // Put HDRezka first if (a === 'hdrezka') return -1; if (b === 'hdrezka') return 1; return 0; @@ -191,16 +177,10 @@ 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 ( = ({ padding: 16, }} > - {/* Backdrop */} = ({ activeOpacity={1} /> - {/* Modal Content */} = ({ modalStyle, ]} > - {/* Glassmorphism Background */} = ({ height: '100%', }} > - {/* Header */} = ({ width: '100%', }} > - + = ({ textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 2, }}> - Switch Source + Video Sources = ({ fontWeight: '500', letterSpacing: 0.2, }}> - Choose from {Object.values(availableStreams).reduce((acc, curr) => acc + curr.streams.length, 0)} available streams + Choose from {Object.values(availableStreams).reduce((acc, curr) => acc + curr.streams.length, 0)} available sources - + - - - - - + + + - {/* Content */} = ({ }} bounces={false} > - {sortedProviders.map(([providerId, { streams, addonName }], providerIndex) => ( - 0 ? 32 : 0, - width: '100%', - }} - > - {/* Provider Header */} + {sortedProviders.map(([providerId, { streams, addonName }]) => ( + - - - - {addonName} - - - Provider • {streams.length} stream{streams.length !== 1 ? 's' : ''} - - - + + {addonName} + {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 ( - { + const isSelected = isStreamSelected(stream); + const quality = getQualityFromTitle(stream.title); + + return ( + + handleStreamSelect(stream)} + activeOpacity={0.85} + disabled={isChangingSource} > - 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... - - - )} - + {stream.title || 'Untitled Stream'} + - {/* Subtitle */} - {!isHDRezka && stream.title && stream.title !== stream.name && ( - - {stream.title} - + {isSelected && ( + + + + PLAYING + + )} - - {/* Enhanced Meta Info */} - - - - {isDolby && ( - - )} - - {isHDR && ( - - )} - - {size && ( - - )} - - {isDebrid && ( - - )} - - {isHDRezka && ( - - )} - - {/* Enhanced Action Icon */} - {isSelected ? ( - - - - ) : ( - - )} + {quality && } + - - - ); - })} - - + + + {isChangingSource ? ( + + ) : ( + + )} + + + + + ); + })} + ))} diff --git a/src/components/player/modals/SubtitleModals.tsx b/src/components/player/modals/SubtitleModals.tsx index 264c0096..455905a7 100644 --- a/src/components/player/modals/SubtitleModals.tsx +++ b/src/components/player/modals/SubtitleModals.tsx @@ -1,23 +1,13 @@ import React from 'react'; import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Image, Dimensions } from 'react-native'; -import { Ionicons, MaterialIcons } from '@expo/vector-icons'; +import { MaterialIcons } from '@expo/vector-icons'; import { BlurView } from 'expo-blur'; import Animated, { FadeIn, - FadeOut, - SlideInDown, - SlideOutDown, - FadeInDown, - FadeInUp, - Layout, - withSpring, - withTiming, + FadeOut, useAnimatedStyle, useSharedValue, - interpolate, - Easing, - withDelay, - withSequence, + withTiming, runOnJS, } from 'react-native-reanimated'; import { LinearGradient } from 'expo-linear-gradient'; @@ -47,7 +37,6 @@ interface SubtitleModalsProps { const { width, height } = Dimensions.get('window'); -// Fixed dimensions for the modals const MODAL_WIDTH = Math.min(width - 32, 520); const MODAL_MAX_HEIGHT = height * 0.85; @@ -55,17 +44,14 @@ const SubtitleBadge = ({ text, color, bgColor, - icon, - delay = 0 + icon }: { text: string; color: string; bgColor: string; icon?: string; - delay?: number; }) => ( - {text} - + ); export const SubtitleModals: React.FC = ({ @@ -115,59 +101,51 @@ export const SubtitleModals: React.FC = ({ increaseSubtitleSize, decreaseSubtitleSize, }) => { - const modalScale = useSharedValue(0.9); const modalOpacity = useSharedValue(0); - const languageModalScale = useSharedValue(0.9); const languageModalOpacity = useSharedValue(0); React.useEffect(() => { if (showSubtitleModal) { - modalScale.value = withSpring(1, { - damping: 20, - stiffness: 300, - mass: 0.8, - }); - modalOpacity.value = withTiming(1, { - duration: 200, - easing: Easing.out(Easing.quad), - }); + modalOpacity.value = withTiming(1, { duration: 200 }); + } else { + modalOpacity.value = withTiming(0, { duration: 150 }); } + + return () => { + modalOpacity.value = 0; + }; }, [showSubtitleModal]); React.useEffect(() => { if (showSubtitleLanguageModal) { - languageModalScale.value = withSpring(1, { - damping: 20, - stiffness: 300, - mass: 0.8, - }); - languageModalOpacity.value = withTiming(1, { - duration: 200, - easing: Easing.out(Easing.quad), - }); + languageModalOpacity.value = withTiming(1, { duration: 200 }); + } else { + languageModalOpacity.value = withTiming(0, { duration: 150 }); } + + return () => { + languageModalOpacity.value = 0; + }; }, [showSubtitleLanguageModal]); const modalStyle = useAnimatedStyle(() => ({ - transform: [{ scale: modalScale.value }], opacity: modalOpacity.value, })); const languageModalStyle = useAnimatedStyle(() => ({ - transform: [{ scale: languageModalScale.value }], opacity: languageModalOpacity.value, })); const handleClose = () => { - modalScale.value = withTiming(0.9, { duration: 150 }); - modalOpacity.value = withTiming(0, { duration: 150 }); - setTimeout(() => setShowSubtitleModal(false), 150); + modalOpacity.value = withTiming(0, { duration: 150 }, () => { + runOnJS(setShowSubtitleModal)(false); + }); }; const handleLanguageClose = () => { - languageModalScale.value = withTiming(0.9, { duration: 150 }); - languageModalOpacity.value = withTiming(0, { duration: 150 }); - setTimeout(() => setShowSubtitleLanguageModal(false), 150); + languageModalOpacity.value = withTiming(0, { duration: 150 }, () => { + runOnJS(setShowSubtitleLanguageModal)(false); + }); }; // Render subtitle settings modal @@ -176,8 +154,8 @@ export const SubtitleModals: React.FC = ({ return ( = ({ padding: 16, }} > - {/* Backdrop */} = ({ activeOpacity={1} /> - {/* Modal Content */} = ({ modalStyle, ]} > - {/* Glassmorphism Background */} = ({ height: '100%', }} > - {/* Header */} = ({ width: '100%', }} > - + = ({ fontWeight: '500', letterSpacing: 0.2, }}> - Configure subtitles and language options + Customize your subtitle experience - + - - - - - + + + - {/* Content */} = ({ bounces={false} > - - {/* External Subtitles Section */} - + + + Size Adjustment + + - - + onPress={decreaseSubtitleSize} + > + + + + - External Subtitles + {subtitleSize} - High quality with size control + Font Size - - - {/* Custom subtitles option */} - {customSubtitles.length > 0 && ( - - { - selectTextTrack(-999); - setShowSubtitleModal(false); - }} - activeOpacity={0.85} - > - - - - - Custom Subtitles - - - {useCustomSubtitles && ( - - - - ACTIVE - - - )} - - - - - - - - - - {useCustomSubtitles ? ( - - - - ) : ( - - )} - - - - - )} - - {/* Search for external subtitles */} - + { - handleClose(); - fetchAvailableSubtitles(); - }} - disabled={isLoadingSubtitleList} - activeOpacity={0.85} - > - - {isLoadingSubtitleList ? ( - - ) : ( - - )} - - {isLoadingSubtitleList ? 'Searching...' : 'Search Online Subtitles'} - - + alignItems: 'center', + borderWidth: 2, + borderColor: 'rgba(255, 255, 255, 0.1)', + }} + onPress={increaseSubtitleSize} + > + - - + + - {/* Subtitle Size Controls */} - {useCustomSubtitles && ( - + + Subtitle Source + + + setShowSubtitleLanguageModal(true)} > - - Size Control + Change Language - - Adjust font size for better readability - - - - - - - - - - - - {subtitleSize}px - - - Font Size - - - - - - - - - - )} - - {/* Available built-in subtitle tracks */} - {vlcTextTracks.length > 0 ? vlcTextTracks.map((track, index) => ( - - { - selectTextTrack(track.id); - handleClose(); - }} - activeOpacity={0.85} - > - - - - - {getTrackDisplayName(track)} - - - {(selectedTextTrack === track.id && !useCustomSubtitles) && ( - - - - ACTIVE - - - )} - - - + + {selectedTextTrack !== -1 && ( - t.id === selectedTextTrack)?.language?.toUpperCase() || 'UNKNOWN'} color="#6B7280" bgColor="rgba(107, 114, 128, 0.15)" - icon="format-size" - delay={50} /> - - - - - {(selectedTextTrack === track.id && !useCustomSubtitles) ? ( - - - - ) : ( - )} - - - )) : ( - - - - No built-in subtitles available - - - Try searching for external subtitles - - - )} + + + + @@ -866,11 +432,11 @@ export const SubtitleModals: React.FC = ({ // Render subtitle language selection modal const renderSubtitleLanguageModal = () => { if (!showSubtitleLanguageModal) return null; - + return ( = ({ padding: 16, }} > - {/* Backdrop */} = ({ activeOpacity={1} /> - {/* Modal Content */} = ({ languageModalStyle, ]} > - {/* Glassmorphism Background */} = ({ height: '100%', }} > - {/* Header */} = ({ width: '100%', }} > - + = ({ textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 2, }}> - Select Language + Subtitle Language = ({ fontWeight: '500', letterSpacing: 0.2, }}> - Choose from {availableSubtitles.length} available languages + Choose from {vlcTextTracks.length} available tracks - + - - - - - + + + - {/* Content */} = ({ }} bounces={false} > - {availableSubtitles.length > 0 ? availableSubtitles.map((subtitle, index) => ( - - + {vlcTextTracks.map((track) => ( + loadWyzieSubtitle(subtitle)} - disabled={isLoadingSubtitles} - activeOpacity={0.85} > - + { + selectTextTrack(track.id); + handleLanguageClose(); + }} + activeOpacity={0.85} + > - - - + - {formatLanguage(subtitle.language)} - - + {getTrackDisplayName(track)} + + + {selectedTextTrack === track.id && ( + + + + ACTIVE + + + )} + + + - {subtitle.display} - + + {track.language && ( + + )} + + + + + - - - {isLoadingSubtitles ? ( - - ) : ( - - )} - - - - - )) : ( - - - - No subtitles found - - - No subtitles are available for this content.{'\n'}Try searching again or check back later. - - - )} + + + ))} +