mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-27 11:23:02 +00:00
Enhance AudioTrackModal with improved animations and visual elements
This update introduces a new AudioBadge component for better visual feedback on audio track options, along with enhanced animations for modal transitions. The modal now features a glassmorphism background and improved layout, providing a more engaging user experience. Additionally, the closing animation has been refined for smoother transitions, enhancing overall usability.
This commit is contained in:
parent
2ae2d4a828
commit
2d71a64af8
1 changed files with 462 additions and 44 deletions
|
|
@ -1,6 +1,28 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
|
import { View, Text, TouchableOpacity, ScrollView, Dimensions } from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
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 { styles } from '../utils/playerStyles';
|
||||||
import { getTrackDisplayName } from '../utils/playerUtils';
|
import { getTrackDisplayName } from '../utils/playerUtils';
|
||||||
|
|
||||||
|
|
@ -12,6 +34,53 @@ interface AudioTrackModalProps {
|
||||||
selectAudioTrack: (trackId: number) => void;
|
selectAudioTrack: (trackId: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { width, height } = Dimensions.get('window');
|
||||||
|
|
||||||
|
const AudioBadge = ({
|
||||||
|
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: 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>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
|
||||||
export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
||||||
showAudioModal,
|
showAudioModal,
|
||||||
setShowAudioModal,
|
setShowAudioModal,
|
||||||
|
|
@ -19,56 +88,405 @@ export const AudioTrackModal: React.FC<AudioTrackModalProps> = ({
|
||||||
selectedAudioTrack,
|
selectedAudioTrack,
|
||||||
selectAudioTrack,
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [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);
|
||||||
|
};
|
||||||
|
|
||||||
if (!showAudioModal) return null;
|
if (!showAudioModal) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.fullscreenOverlay}>
|
<Animated.View
|
||||||
<View style={styles.enhancedModalContainer}>
|
entering={FadeIn.duration(250)}
|
||||||
<View style={styles.enhancedModalHeader}>
|
exiting={FadeOut.duration(200)}
|
||||||
<Text style={styles.enhancedModalTitle}>Audio</Text>
|
style={{
|
||||||
<TouchableOpacity
|
position: 'absolute',
|
||||||
style={styles.enhancedCloseButton}
|
top: 0,
|
||||||
onPress={() => setShowAudioModal(false)}
|
left: 0,
|
||||||
>
|
right: 0,
|
||||||
<Ionicons name="close" size={24} color="white" />
|
bottom: 0,
|
||||||
</TouchableOpacity>
|
backgroundColor: 'rgba(0, 0, 0, 0.9)',
|
||||||
</View>
|
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}
|
||||||
|
/>
|
||||||
|
|
||||||
<ScrollView style={styles.trackListScrollContainer}>
|
{/* Modal Content */}
|
||||||
<View style={styles.trackListContainer}>
|
<Animated.View
|
||||||
{vlcAudioTracks.length > 0 ? vlcAudioTracks.map(track => (
|
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(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)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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,
|
||||||
|
}}>
|
||||||
|
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>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
<Animated.View entering={BounceIn.duration(400).delay(200)}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={track.id}
|
style={{
|
||||||
style={styles.enhancedTrackItem}
|
width: 44,
|
||||||
onPress={() => {
|
height: 44,
|
||||||
selectAudioTrack(track.id);
|
borderRadius: 22,
|
||||||
setShowAudioModal(false);
|
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}
|
||||||
>
|
>
|
||||||
<View style={styles.trackInfoContainer}>
|
<MaterialIcons name="close" size={20} color="#fff" />
|
||||||
<Text style={styles.trackPrimaryText}>
|
|
||||||
{getTrackDisplayName(track)}
|
|
||||||
</Text>
|
|
||||||
{(track.name && track.language) && (
|
|
||||||
<Text style={styles.trackSecondaryText}>{track.name}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
{selectedAudioTrack === track.id && (
|
|
||||||
<View style={styles.selectedIndicatorContainer}>
|
|
||||||
<Ionicons name="checkmark" size={22} color="#E50914" />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)) : (
|
</Animated.View>
|
||||||
<View style={styles.emptyStateContainer}>
|
</LinearGradient>
|
||||||
<Ionicons name="alert-circle-outline" size={40} color="#888" />
|
|
||||||
<Text style={styles.emptyStateText}>No audio tracks available</Text>
|
{/* Content */}
|
||||||
|
<ScrollView
|
||||||
|
style={{
|
||||||
|
maxHeight: height * 0.6,
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
}}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
contentContainerStyle={{
|
||||||
|
padding: 24,
|
||||||
|
paddingBottom: 32,
|
||||||
|
}}
|
||||||
|
bounces={false}
|
||||||
|
>
|
||||||
|
{/* Audio Tracks Section */}
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.duration(400).delay(150)}
|
||||||
|
layout={Layout.springify()}
|
||||||
|
style={{
|
||||||
|
marginBottom: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 20,
|
||||||
|
paddingBottom: 12,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
}}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={['#F97316', '#EA580C']}
|
||||||
|
style={{
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
borderRadius: 6,
|
||||||
|
marginRight: 16,
|
||||||
|
elevation: 3,
|
||||||
|
shadowColor: '#F97316',
|
||||||
|
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,
|
||||||
|
}}>
|
||||||
|
Available Audio Tracks
|
||||||
|
</Text>
|
||||||
|
<Text style={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.6)',
|
||||||
|
fontSize: 12,
|
||||||
|
marginTop: 1,
|
||||||
|
fontWeight: '500',
|
||||||
|
}}>
|
||||||
|
Select your preferred audio language
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)}
|
|
||||||
</View>
|
{/* Audio Tracks List */}
|
||||||
</ScrollView>
|
{vlcAudioTracks.length > 0 ? vlcAudioTracks.map((track, index) => {
|
||||||
</View>
|
const isSelected = selectedAudioTrack === track.id;
|
||||||
</View>
|
|
||||||
|
return (
|
||||||
|
<Animated.View
|
||||||
|
key={track.id}
|
||||||
|
entering={FadeInDown.duration(300).delay(200 + (index * 50))}
|
||||||
|
layout={Layout.springify()}
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
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,
|
||||||
|
}}
|
||||||
|
onPress={() => {
|
||||||
|
selectAudioTrack(track.id);
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
>
|
||||||
|
<View style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<View style={{ flex: 1, marginRight: 16 }}>
|
||||||
|
<View style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 8,
|
||||||
|
gap: 12,
|
||||||
|
}}>
|
||||||
|
<Text style={{
|
||||||
|
color: isSelected ? '#fff' : 'rgba(255, 255, 255, 0.95)',
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '700',
|
||||||
|
letterSpacing: -0.2,
|
||||||
|
flex: 1,
|
||||||
|
}}>
|
||||||
|
{getTrackDisplayName(track)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{isSelected && (
|
||||||
|
<Animated.View
|
||||||
|
entering={BounceIn.duration(300)}
|
||||||
|
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>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{(track.name && track.language) && (
|
||||||
|
<Text style={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.65)',
|
||||||
|
fontSize: 13,
|
||||||
|
marginBottom: 8,
|
||||||
|
lineHeight: 18,
|
||||||
|
fontWeight: '400',
|
||||||
|
}}>
|
||||||
|
{track.name}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<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"
|
||||||
|
delay={50}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</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',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: isSelected
|
||||||
|
? 'rgba(249, 115, 22, 0.3)'
|
||||||
|
: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
}}>
|
||||||
|
{isSelected ? (
|
||||||
|
<Animated.View entering={ZoomIn.duration(200)}>
|
||||||
|
<MaterialIcons name="check-circle" size={24} color="#F97316" />
|
||||||
|
</Animated.View>
|
||||||
|
) : (
|
||||||
|
<MaterialIcons name="volume-up" size={24} color="rgba(255,255,255,0.6)" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
}) : (
|
||||||
|
<Animated.View
|
||||||
|
entering={FadeInDown.duration(300).delay(200)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||||
|
borderRadius: 20,
|
||||||
|
padding: 40,
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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 available
|
||||||
|
</Text>
|
||||||
|
<Text style={{
|
||||||
|
color: 'rgba(255, 255, 255, 0.4)',
|
||||||
|
fontSize: 14,
|
||||||
|
marginTop: 8,
|
||||||
|
textAlign: 'center',
|
||||||
|
lineHeight: 20,
|
||||||
|
}}>
|
||||||
|
This content doesn't have multiple audio tracks.{'\n'}The default audio will be used.
|
||||||
|
</Text>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</Animated.View>
|
||||||
|
</ScrollView>
|
||||||
|
</BlurView>
|
||||||
|
</Animated.View>
|
||||||
|
</Animated.View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue