improved parental guide UI

This commit is contained in:
tapframe 2025-12-25 14:22:12 +05:30
parent eee6f81fca
commit 7e7804b6d4
5 changed files with 17 additions and 531 deletions

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -1,5 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { View, Text, StyleSheet, Dimensions } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
@ -10,6 +10,7 @@ import Animated, {
} from 'react-native-reanimated';
import { parentalGuideService } from '../../../services/parentalGuideService';
import { logger } from '../../../utils/logger';
import { useTheme } from '../../../contexts/ThemeContext';
interface ParentalGuideOverlayProps {
imdbId: string | undefined;
@ -42,16 +43,17 @@ const ROW_HEIGHT = 18;
const WarningItemView: React.FC<{
item: WarningItem;
opacity: SharedValue<number>;
}> = ({ item, opacity }) => {
fontSize: number;
}> = ({ item, opacity, fontSize }) => {
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value,
}));
return (
<Animated.View style={[styles.warningItem, animatedStyle]}>
<Text style={styles.label}>{item.label}</Text>
<Text style={styles.separator}>·</Text>
<Text style={styles.severity}>{item.severity}</Text>
<Text style={[styles.label, { fontSize }]}>{item.label}</Text>
<Text style={[styles.separator, { fontSize }]}>·</Text>
<Text style={[styles.severity, { fontSize }]}>{item.severity}</Text>
</Animated.View>
);
};
@ -63,6 +65,8 @@ export const ParentalGuideOverlay: React.FC<ParentalGuideOverlayProps> = ({
episode,
shouldShow,
}) => {
const { currentTheme } = useTheme();
const screenWidth = Dimensions.get('window').width;
const [warnings, setWarnings] = useState<WarningItem[]>([]);
const [isVisible, setIsVisible] = useState(false);
const hasShownRef = useRef(false);
@ -222,10 +226,15 @@ export const ParentalGuideOverlay: React.FC<ParentalGuideOverlayProps> = ({
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 (
<Animated.View style={[styles.container, containerStyle]} pointerEvents="none">
<Animated.View style={[styles.container, { left: containerPadding, top: containerPadding + 30 }]} pointerEvents="none">
{/* Vertical line - animates height */}
<Animated.View style={[styles.line, lineStyle]} />
<Animated.View style={[styles.line, lineStyle, { backgroundColor: currentTheme.colors.primary, width: lineWidth }]} />
{/* Warning items */}
<View style={styles.itemsContainer}>
@ -234,6 +243,7 @@ export const ParentalGuideOverlay: React.FC<ParentalGuideOverlayProps> = ({
key={item.label}
item={item}
opacity={itemOpacities[index]}
fontSize={fontSize}
/>
))}
</View>
@ -244,15 +254,11 @@ export const ParentalGuideOverlay: React.FC<ParentalGuideOverlayProps> = ({
const styles = StyleSheet.create({
container: {
position: 'absolute',
top: 50,
left: 16,
flexDirection: 'row',
alignItems: 'flex-start',
zIndex: 100,
},
line: {
width: 2,
backgroundColor: 'rgba(255, 255, 255, 0.5)',
borderRadius: 1,
marginRight: 10,
},