mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
improved parental guide UI
This commit is contained in:
parent
eee6f81fca
commit
7e7804b6d4
5 changed files with 17 additions and 531 deletions
|
|
@ -1,228 +0,0 @@
|
||||||
import React, { useState, useRef } from 'react';
|
|
||||||
import { View, Text, TouchableOpacity, ScrollView, Animated, StyleSheet } from 'react-native';
|
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
|
||||||
import FastImage from '@d11/react-native-fast-image';
|
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
interface PauseOverlayProps {
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
title: string;
|
|
||||||
episodeTitle?: string;
|
|
||||||
season?: number;
|
|
||||||
episode?: number;
|
|
||||||
year?: string | number;
|
|
||||||
type: string;
|
|
||||||
description: string;
|
|
||||||
cast: any[];
|
|
||||||
screenDimensions: { width: number, height: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PauseOverlay: React.FC<PauseOverlayProps> = ({
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
title,
|
|
||||||
episodeTitle,
|
|
||||||
season,
|
|
||||||
episode,
|
|
||||||
year,
|
|
||||||
type,
|
|
||||||
description,
|
|
||||||
cast,
|
|
||||||
screenDimensions
|
|
||||||
}) => {
|
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
|
|
||||||
// Internal Animation State
|
|
||||||
const pauseOverlayOpacity = useRef(new Animated.Value(visible ? 1 : 0)).current;
|
|
||||||
const pauseOverlayTranslateY = useRef(new Animated.Value(12)).current;
|
|
||||||
const metadataOpacity = useRef(new Animated.Value(1)).current;
|
|
||||||
const metadataScale = useRef(new Animated.Value(1)).current;
|
|
||||||
|
|
||||||
// Cast Details State
|
|
||||||
const [selectedCastMember, setSelectedCastMember] = useState<any>(null);
|
|
||||||
const [showCastDetails, setShowCastDetails] = useState(false);
|
|
||||||
const castDetailsOpacity = useRef(new Animated.Value(0)).current;
|
|
||||||
const castDetailsScale = useRef(new Animated.Value(0.95)).current;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
Animated.timing(pauseOverlayOpacity, {
|
|
||||||
toValue: visible ? 1 : 0,
|
|
||||||
duration: 250,
|
|
||||||
useNativeDriver: true
|
|
||||||
}).start();
|
|
||||||
}, [visible]);
|
|
||||||
|
|
||||||
if (!visible && !showCastDetails) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
activeOpacity={1}
|
|
||||||
onPress={onClose}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
zIndex: 30,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Animated.View
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
opacity: pauseOverlayOpacity,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Horizontal Fade */}
|
|
||||||
<View style={{ position: 'absolute', top: 0, left: 0, bottom: 0, width: screenDimensions.width * 0.7 }}>
|
|
||||||
<LinearGradient
|
|
||||||
start={{ x: 0, y: 0.5 }}
|
|
||||||
end={{ x: 1, y: 0.5 }}
|
|
||||||
colors={['rgba(0,0,0,0.85)', 'rgba(0,0,0,0.0)']}
|
|
||||||
locations={[0, 1]}
|
|
||||||
style={StyleSheet.absoluteFill}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[
|
|
||||||
'rgba(0,0,0,0.6)',
|
|
||||||
'rgba(0,0,0,0.4)',
|
|
||||||
'rgba(0,0,0,0.2)',
|
|
||||||
'rgba(0,0,0,0.0)'
|
|
||||||
]}
|
|
||||||
locations={[0, 0.3, 0.6, 1]}
|
|
||||||
style={StyleSheet.absoluteFill}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Animated.View style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: 24 + insets.left,
|
|
||||||
right: 24 + insets.right,
|
|
||||||
top: 24 + insets.top,
|
|
||||||
bottom: 110 + insets.bottom,
|
|
||||||
transform: [{ translateY: pauseOverlayTranslateY }]
|
|
||||||
}}>
|
|
||||||
{showCastDetails && selectedCastMember ? (
|
|
||||||
<Animated.View
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
opacity: castDetailsOpacity,
|
|
||||||
transform: [{ scale: castDetailsScale }]
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View style={{ alignItems: 'flex-start', paddingBottom: screenDimensions.height * 0.1 }}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 24, paddingVertical: 8, paddingHorizontal: 4 }}
|
|
||||||
onPress={() => {
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(castDetailsOpacity, { toValue: 0, duration: 250, useNativeDriver: true }),
|
|
||||||
Animated.timing(castDetailsScale, { toValue: 0.95, duration: 250, useNativeDriver: true })
|
|
||||||
]).start(() => {
|
|
||||||
setShowCastDetails(false);
|
|
||||||
setSelectedCastMember(null);
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(metadataOpacity, { toValue: 1, duration: 400, useNativeDriver: true }),
|
|
||||||
Animated.spring(metadataScale, { toValue: 1, tension: 80, friction: 8, useNativeDriver: true })
|
|
||||||
]).start();
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MaterialIcons name="arrow-back" size={20} color="#FFFFFF" style={{ marginRight: 8 }} />
|
|
||||||
<Text style={{ color: '#B8B8B8', fontSize: Math.min(14, screenDimensions.width * 0.02) }}>Back to details</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'flex-start', width: '100%' }}>
|
|
||||||
{selectedCastMember.profile_path && (
|
|
||||||
<View style={{ marginRight: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8, elevation: 5 }}>
|
|
||||||
<FastImage
|
|
||||||
source={{ uri: `https://image.tmdb.org/t/p/w300${selectedCastMember.profile_path}` }}
|
|
||||||
style={{ width: Math.min(120, screenDimensions.width * 0.18), height: Math.min(180, screenDimensions.width * 0.27), borderRadius: 12, backgroundColor: 'rgba(255,255,255,0.1)' }}
|
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View style={{ flex: 1, paddingTop: 8 }}>
|
|
||||||
<Text style={{ color: '#FFFFFF', fontSize: Math.min(32, screenDimensions.width * 0.045), fontWeight: '800', marginBottom: 8 }} numberOfLines={2}>
|
|
||||||
{selectedCastMember.name}
|
|
||||||
</Text>
|
|
||||||
{selectedCastMember.character && (
|
|
||||||
<Text style={{ color: '#CCCCCC', fontSize: Math.min(16, screenDimensions.width * 0.022), marginBottom: 8, fontWeight: '500', fontStyle: 'italic' }} numberOfLines={2}>
|
|
||||||
as {selectedCastMember.character}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{selectedCastMember.biography && (
|
|
||||||
<Text style={{ color: '#D6D6D6', fontSize: Math.min(14, screenDimensions.width * 0.019), lineHeight: Math.min(20, screenDimensions.width * 0.026), marginTop: 16, opacity: 0.9 }} numberOfLines={4}>
|
|
||||||
{selectedCastMember.biography}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
) : (
|
|
||||||
<Animated.View style={{ flex: 1, justifyContent: 'space-between', opacity: metadataOpacity, transform: [{ scale: metadataScale }] }}>
|
|
||||||
<View>
|
|
||||||
<Text style={{ color: '#B8B8B8', fontSize: Math.min(18, screenDimensions.width * 0.025), marginBottom: 8 }}>You're watching</Text>
|
|
||||||
<Text style={{ color: '#FFFFFF', fontSize: Math.min(48, screenDimensions.width * 0.06), fontWeight: '800', marginBottom: 10 }} numberOfLines={2}>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
{!!year && (
|
|
||||||
<Text style={{ color: '#CCCCCC', fontSize: Math.min(18, screenDimensions.width * 0.025), marginBottom: 8 }} numberOfLines={1}>
|
|
||||||
{`${year}${type === 'series' && season && episode ? ` • S${season}E${episode}` : ''}`}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{!!episodeTitle && (
|
|
||||||
<Text style={{ color: '#FFFFFF', fontSize: Math.min(20, screenDimensions.width * 0.03), fontWeight: '600', marginBottom: 8 }} numberOfLines={2}>
|
|
||||||
{episodeTitle}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{description && (
|
|
||||||
<Text style={{ color: '#D6D6D6', fontSize: Math.min(18, screenDimensions.width * 0.025), lineHeight: Math.min(24, screenDimensions.width * 0.03) }} numberOfLines={3}>
|
|
||||||
{description}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{cast && cast.length > 0 && (
|
|
||||||
<View style={{ marginTop: 16 }}>
|
|
||||||
<Text style={{ color: '#B8B8B8', fontSize: Math.min(16, screenDimensions.width * 0.022), marginBottom: 8 }}>Cast</Text>
|
|
||||||
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
|
||||||
{cast.slice(0, 6).map((castMember: any, index: number) => (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={castMember.id || index}
|
|
||||||
style={{ backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 12, paddingHorizontal: Math.min(12, screenDimensions.width * 0.015), paddingVertical: Math.min(6, screenDimensions.height * 0.008), marginRight: 8, marginBottom: 8 }}
|
|
||||||
onPress={() => {
|
|
||||||
setSelectedCastMember(castMember);
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(metadataOpacity, { toValue: 0, duration: 250, useNativeDriver: true }),
|
|
||||||
Animated.timing(metadataScale, { toValue: 0.95, duration: 250, useNativeDriver: true })
|
|
||||||
]).start(() => {
|
|
||||||
setShowCastDetails(true);
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(castDetailsOpacity, { toValue: 1, duration: 400, useNativeDriver: true }),
|
|
||||||
Animated.spring(castDetailsScale, { toValue: 1, tension: 80, friction: 8, useNativeDriver: true })
|
|
||||||
]).start();
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ color: '#FFFFFF', fontSize: Math.min(14, screenDimensions.width * 0.018) }}>
|
|
||||||
{castMember.name}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
)}
|
|
||||||
</Animated.View>
|
|
||||||
</Animated.View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { View, Text, Animated, StyleSheet } from 'react-native';
|
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
|
||||||
import { styles } from '../../utils/playerStyles';
|
|
||||||
|
|
||||||
interface SpeedActivatedOverlayProps {
|
|
||||||
visible: boolean;
|
|
||||||
opacity: Animated.Value;
|
|
||||||
speed: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SpeedActivatedOverlay: React.FC<SpeedActivatedOverlayProps> = ({
|
|
||||||
visible,
|
|
||||||
opacity,
|
|
||||||
speed
|
|
||||||
}) => {
|
|
||||||
if (!visible) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.speedActivatedOverlay,
|
|
||||||
{ opacity: opacity }
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View style={styles.speedActivatedContainer}>
|
|
||||||
<MaterialIcons name="fast-forward" size={32} color="#FFFFFF" />
|
|
||||||
<Text style={styles.speedActivatedText}>{speed}x Speed</Text>
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,228 +0,0 @@
|
||||||
import React, { useState, useRef } from 'react';
|
|
||||||
import { View, Text, TouchableOpacity, Animated, StyleSheet } from 'react-native';
|
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
|
||||||
import FastImage from '@d11/react-native-fast-image';
|
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
interface PauseOverlayProps {
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
title: string;
|
|
||||||
episodeTitle?: string;
|
|
||||||
season?: number;
|
|
||||||
episode?: number;
|
|
||||||
year?: string | number;
|
|
||||||
type: string;
|
|
||||||
description: string;
|
|
||||||
cast: any[];
|
|
||||||
screenDimensions: { width: number, height: number };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PauseOverlay: React.FC<PauseOverlayProps> = ({
|
|
||||||
visible,
|
|
||||||
onClose,
|
|
||||||
title,
|
|
||||||
episodeTitle,
|
|
||||||
season,
|
|
||||||
episode,
|
|
||||||
year,
|
|
||||||
type,
|
|
||||||
description,
|
|
||||||
cast,
|
|
||||||
screenDimensions
|
|
||||||
}) => {
|
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
|
|
||||||
// Internal Animation State
|
|
||||||
const pauseOverlayOpacity = useRef(new Animated.Value(visible ? 1 : 0)).current;
|
|
||||||
const pauseOverlayTranslateY = useRef(new Animated.Value(12)).current;
|
|
||||||
const metadataOpacity = useRef(new Animated.Value(1)).current;
|
|
||||||
const metadataScale = useRef(new Animated.Value(1)).current;
|
|
||||||
|
|
||||||
// Cast Details State
|
|
||||||
const [selectedCastMember, setSelectedCastMember] = useState<any>(null);
|
|
||||||
const [showCastDetails, setShowCastDetails] = useState(false);
|
|
||||||
const castDetailsOpacity = useRef(new Animated.Value(0)).current;
|
|
||||||
const castDetailsScale = useRef(new Animated.Value(0.95)).current;
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
Animated.timing(pauseOverlayOpacity, {
|
|
||||||
toValue: visible ? 1 : 0,
|
|
||||||
duration: 250,
|
|
||||||
useNativeDriver: true
|
|
||||||
}).start();
|
|
||||||
}, [visible]);
|
|
||||||
|
|
||||||
if (!visible && !showCastDetails) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
activeOpacity={1}
|
|
||||||
onPress={onClose}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
zIndex: 30,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Animated.View
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
opacity: pauseOverlayOpacity,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* Horizontal Fade */}
|
|
||||||
<View style={{ position: 'absolute', top: 0, left: 0, bottom: 0, width: screenDimensions.width * 0.7 }}>
|
|
||||||
<LinearGradient
|
|
||||||
start={{ x: 0, y: 0.5 }}
|
|
||||||
end={{ x: 1, y: 0.5 }}
|
|
||||||
colors={['rgba(0,0,0,0.85)', 'rgba(0,0,0,0.0)']}
|
|
||||||
locations={[0, 1]}
|
|
||||||
style={StyleSheet.absoluteFill}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[
|
|
||||||
'rgba(0,0,0,0.6)',
|
|
||||||
'rgba(0,0,0,0.4)',
|
|
||||||
'rgba(0,0,0,0.2)',
|
|
||||||
'rgba(0,0,0,0.0)'
|
|
||||||
]}
|
|
||||||
locations={[0, 0.3, 0.6, 1]}
|
|
||||||
style={StyleSheet.absoluteFill}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Animated.View style={{
|
|
||||||
position: 'absolute',
|
|
||||||
left: 24 + insets.left,
|
|
||||||
right: 24 + insets.right,
|
|
||||||
top: 24 + insets.top,
|
|
||||||
bottom: 110 + insets.bottom,
|
|
||||||
transform: [{ translateY: pauseOverlayTranslateY }]
|
|
||||||
}}>
|
|
||||||
{showCastDetails && selectedCastMember ? (
|
|
||||||
<Animated.View
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
opacity: castDetailsOpacity,
|
|
||||||
transform: [{ scale: castDetailsScale }]
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View style={{ alignItems: 'flex-start', paddingBottom: screenDimensions.height * 0.1 }}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={{ flexDirection: 'row', alignItems: 'center', marginBottom: 24, paddingVertical: 8, paddingHorizontal: 4 }}
|
|
||||||
onPress={() => {
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(castDetailsOpacity, { toValue: 0, duration: 250, useNativeDriver: true }),
|
|
||||||
Animated.timing(castDetailsScale, { toValue: 0.95, duration: 250, useNativeDriver: true })
|
|
||||||
]).start(() => {
|
|
||||||
setShowCastDetails(false);
|
|
||||||
setSelectedCastMember(null);
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(metadataOpacity, { toValue: 1, duration: 400, useNativeDriver: true }),
|
|
||||||
Animated.spring(metadataScale, { toValue: 1, tension: 80, friction: 8, useNativeDriver: true })
|
|
||||||
]).start();
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MaterialIcons name="arrow-back" size={20} color="#FFFFFF" style={{ marginRight: 8 }} />
|
|
||||||
<Text style={{ color: '#B8B8B8', fontSize: Math.min(14, screenDimensions.width * 0.02) }}>Back to details</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'flex-start', width: '100%' }}>
|
|
||||||
{selectedCastMember.profile_path && (
|
|
||||||
<View style={{ marginRight: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8, elevation: 5 }}>
|
|
||||||
<FastImage
|
|
||||||
source={{ uri: `https://image.tmdb.org/t/p/w300${selectedCastMember.profile_path}` }}
|
|
||||||
style={{ width: Math.min(120, screenDimensions.width * 0.18), height: Math.min(180, screenDimensions.width * 0.27), borderRadius: 12, backgroundColor: 'rgba(255,255,255,0.1)' }}
|
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<View style={{ flex: 1, paddingTop: 8 }}>
|
|
||||||
<Text style={{ color: '#FFFFFF', fontSize: Math.min(32, screenDimensions.width * 0.045), fontWeight: '800', marginBottom: 8 }} numberOfLines={2}>
|
|
||||||
{selectedCastMember.name}
|
|
||||||
</Text>
|
|
||||||
{selectedCastMember.character && (
|
|
||||||
<Text style={{ color: '#CCCCCC', fontSize: Math.min(16, screenDimensions.width * 0.022), marginBottom: 8, fontWeight: '500', fontStyle: 'italic' }} numberOfLines={2}>
|
|
||||||
as {selectedCastMember.character}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{selectedCastMember.biography && (
|
|
||||||
<Text style={{ color: '#D6D6D6', fontSize: Math.min(14, screenDimensions.width * 0.019), lineHeight: Math.min(20, screenDimensions.width * 0.026), marginTop: 16, opacity: 0.9 }} numberOfLines={4}>
|
|
||||||
{selectedCastMember.biography}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
) : (
|
|
||||||
<Animated.View style={{ flex: 1, justifyContent: 'space-between', opacity: metadataOpacity, transform: [{ scale: metadataScale }] }}>
|
|
||||||
<View>
|
|
||||||
<Text style={{ color: '#B8B8B8', fontSize: Math.min(18, screenDimensions.width * 0.025), marginBottom: 8 }}>You're watching</Text>
|
|
||||||
<Text style={{ color: '#FFFFFF', fontSize: Math.min(48, screenDimensions.width * 0.06), fontWeight: '800', marginBottom: 10 }} numberOfLines={2}>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
{!!year && (
|
|
||||||
<Text style={{ color: '#CCCCCC', fontSize: Math.min(18, screenDimensions.width * 0.025), marginBottom: 8 }} numberOfLines={1}>
|
|
||||||
{`${year}${type === 'series' && season && episode ? ` • S${season}E${episode}` : ''}`}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{!!episodeTitle && (
|
|
||||||
<Text style={{ color: '#FFFFFF', fontSize: Math.min(20, screenDimensions.width * 0.03), fontWeight: '600', marginBottom: 8 }} numberOfLines={2}>
|
|
||||||
{episodeTitle}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{description && (
|
|
||||||
<Text style={{ color: '#D6D6D6', fontSize: Math.min(18, screenDimensions.width * 0.025), lineHeight: Math.min(24, screenDimensions.width * 0.03) }} numberOfLines={3}>
|
|
||||||
{description}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{cast && cast.length > 0 && (
|
|
||||||
<View style={{ marginTop: 16 }}>
|
|
||||||
<Text style={{ color: '#B8B8B8', fontSize: Math.min(16, screenDimensions.width * 0.022), marginBottom: 8 }}>Cast</Text>
|
|
||||||
<View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
|
|
||||||
{cast.slice(0, 6).map((castMember: any, index: number) => (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={castMember.id || index}
|
|
||||||
style={{ backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 12, paddingHorizontal: Math.min(12, screenDimensions.width * 0.015), paddingVertical: Math.min(6, screenDimensions.height * 0.008), marginRight: 8, marginBottom: 8 }}
|
|
||||||
onPress={() => {
|
|
||||||
setSelectedCastMember(castMember);
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(metadataOpacity, { toValue: 0, duration: 250, useNativeDriver: true }),
|
|
||||||
Animated.timing(metadataScale, { toValue: 0.95, duration: 250, useNativeDriver: true })
|
|
||||||
]).start(() => {
|
|
||||||
setShowCastDetails(true);
|
|
||||||
Animated.parallel([
|
|
||||||
Animated.timing(castDetailsOpacity, { toValue: 1, duration: 400, useNativeDriver: true }),
|
|
||||||
Animated.spring(castDetailsScale, { toValue: 1, tension: 80, friction: 8, useNativeDriver: true })
|
|
||||||
]).start();
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={{ color: '#FFFFFF', fontSize: Math.min(14, screenDimensions.width * 0.018) }}>
|
|
||||||
{castMember.name}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
)}
|
|
||||||
</Animated.View>
|
|
||||||
</Animated.View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { View, Text, Animated } from 'react-native';
|
|
||||||
import { MaterialIcons } from '@expo/vector-icons';
|
|
||||||
import { styles } from '../../utils/playerStyles';
|
|
||||||
|
|
||||||
interface SpeedActivatedOverlayProps {
|
|
||||||
visible: boolean;
|
|
||||||
opacity: Animated.Value;
|
|
||||||
speed: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SpeedActivatedOverlay: React.FC<SpeedActivatedOverlayProps> = ({
|
|
||||||
visible,
|
|
||||||
opacity,
|
|
||||||
speed
|
|
||||||
}) => {
|
|
||||||
if (!visible) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
styles.speedActivatedOverlay,
|
|
||||||
{ opacity: opacity }
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<View style={styles.speedActivatedContainer}>
|
|
||||||
<MaterialIcons name="fast-forward" size={32} color="#FFFFFF" />
|
|
||||||
<Text style={styles.speedActivatedText}>{speed}x Speed</Text>
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { View, Text, StyleSheet } from 'react-native';
|
import { View, Text, StyleSheet, Dimensions } from 'react-native';
|
||||||
import Animated, {
|
import Animated, {
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
|
|
@ -10,6 +10,7 @@ import Animated, {
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { parentalGuideService } from '../../../services/parentalGuideService';
|
import { parentalGuideService } from '../../../services/parentalGuideService';
|
||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
|
import { useTheme } from '../../../contexts/ThemeContext';
|
||||||
|
|
||||||
interface ParentalGuideOverlayProps {
|
interface ParentalGuideOverlayProps {
|
||||||
imdbId: string | undefined;
|
imdbId: string | undefined;
|
||||||
|
|
@ -42,16 +43,17 @@ const ROW_HEIGHT = 18;
|
||||||
const WarningItemView: React.FC<{
|
const WarningItemView: React.FC<{
|
||||||
item: WarningItem;
|
item: WarningItem;
|
||||||
opacity: SharedValue<number>;
|
opacity: SharedValue<number>;
|
||||||
}> = ({ item, opacity }) => {
|
fontSize: number;
|
||||||
|
}> = ({ item, opacity, fontSize }) => {
|
||||||
const animatedStyle = useAnimatedStyle(() => ({
|
const animatedStyle = useAnimatedStyle(() => ({
|
||||||
opacity: opacity.value,
|
opacity: opacity.value,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[styles.warningItem, animatedStyle]}>
|
<Animated.View style={[styles.warningItem, animatedStyle]}>
|
||||||
<Text style={styles.label}>{item.label}</Text>
|
<Text style={[styles.label, { fontSize }]}>{item.label}</Text>
|
||||||
<Text style={styles.separator}>·</Text>
|
<Text style={[styles.separator, { fontSize }]}>·</Text>
|
||||||
<Text style={styles.severity}>{item.severity}</Text>
|
<Text style={[styles.severity, { fontSize }]}>{item.severity}</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -63,6 +65,8 @@ export const ParentalGuideOverlay: React.FC<ParentalGuideOverlayProps> = ({
|
||||||
episode,
|
episode,
|
||||||
shouldShow,
|
shouldShow,
|
||||||
}) => {
|
}) => {
|
||||||
|
const { currentTheme } = useTheme();
|
||||||
|
const screenWidth = Dimensions.get('window').width;
|
||||||
const [warnings, setWarnings] = useState<WarningItem[]>([]);
|
const [warnings, setWarnings] = useState<WarningItem[]>([]);
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
const hasShownRef = useRef(false);
|
const hasShownRef = useRef(false);
|
||||||
|
|
@ -222,10 +226,15 @@ export const ParentalGuideOverlay: React.FC<ParentalGuideOverlayProps> = ({
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Responsive sizing
|
||||||
|
const fontSize = Math.min(11, screenWidth * 0.014);
|
||||||
|
const lineWidth = Math.min(3, screenWidth * 0.0038);
|
||||||
|
const containerPadding = Math.min(20, screenWidth * 0.025);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View style={[styles.container, containerStyle]} pointerEvents="none">
|
<Animated.View style={[styles.container, { left: containerPadding, top: containerPadding + 30 }]} pointerEvents="none">
|
||||||
{/* Vertical line - animates height */}
|
{/* Vertical line - animates height */}
|
||||||
<Animated.View style={[styles.line, lineStyle]} />
|
<Animated.View style={[styles.line, lineStyle, { backgroundColor: currentTheme.colors.primary, width: lineWidth }]} />
|
||||||
|
|
||||||
{/* Warning items */}
|
{/* Warning items */}
|
||||||
<View style={styles.itemsContainer}>
|
<View style={styles.itemsContainer}>
|
||||||
|
|
@ -234,6 +243,7 @@ export const ParentalGuideOverlay: React.FC<ParentalGuideOverlayProps> = ({
|
||||||
key={item.label}
|
key={item.label}
|
||||||
item={item}
|
item={item}
|
||||||
opacity={itemOpacities[index]}
|
opacity={itemOpacities[index]}
|
||||||
|
fontSize={fontSize}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
@ -244,15 +254,11 @@ export const ParentalGuideOverlay: React.FC<ParentalGuideOverlayProps> = ({
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 50,
|
|
||||||
left: 16,
|
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
},
|
},
|
||||||
line: {
|
line: {
|
||||||
width: 2,
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.5)',
|
|
||||||
borderRadius: 1,
|
borderRadius: 1,
|
||||||
marginRight: 10,
|
marginRight: 10,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue