Pause Overlay Localization Patch

This commit is contained in:
cyberalby2 2026-03-06 23:44:08 +01:00
parent b69e34f1bb
commit a8e55d3e98
3 changed files with 446 additions and 244 deletions

View file

@ -1,259 +1,449 @@
import React, { useState, useRef, useEffect } from 'react';
import { View, Text, TouchableOpacity, ScrollView, Animated, StyleSheet } from 'react-native';
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';
import { useTranslation } from 'react-i18next';
// Delay before showing pause overlay (in milliseconds)
const PAUSE_OVERLAY_DELAY = 5000;
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 };
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
visible,
onClose,
title,
episodeTitle,
season,
episode,
year,
type,
description,
cast,
screenDimensions,
}) => {
const insets = useSafeAreaInsets();
const insets = useSafeAreaInsets();
// Internal state to track if overlay should actually be shown (after delay)
const [shouldShow, setShouldShow] = useState(false);
const delayTimerRef = useRef<NodeJS.Timeout | null>(null);
// Internal state to track if overlay should actually be shown (after delay)
const [shouldShow, setShouldShow] = useState(false);
const delayTimerRef = useRef<NodeJS.Timeout | null>(null);
const { t } = useTranslation();
// Handle delay logic - show overlay only after paused for 5 seconds
useEffect(() => {
if (visible) {
// Start timer to show overlay after delay
delayTimerRef.current = setTimeout(() => {
setShouldShow(true);
}, PAUSE_OVERLAY_DELAY);
} else {
// Immediately hide when not paused
if (delayTimerRef.current) {
clearTimeout(delayTimerRef.current);
delayTimerRef.current = null;
}
setShouldShow(false);
}
// Handle delay logic - show overlay only after paused for 5 seconds
useEffect(() => {
if (visible) {
// Start timer to show overlay after delay
delayTimerRef.current = setTimeout(() => {
setShouldShow(true);
}, PAUSE_OVERLAY_DELAY);
} else {
// Immediately hide when not paused
if (delayTimerRef.current) {
clearTimeout(delayTimerRef.current);
delayTimerRef.current = null;
}
setShouldShow(false);
}
return () => {
if (delayTimerRef.current) {
clearTimeout(delayTimerRef.current);
delayTimerRef.current = null;
}
};
}, [visible]);
return () => {
if (delayTimerRef.current) {
clearTimeout(delayTimerRef.current);
delayTimerRef.current = null;
}
};
}, [visible]);
// Internal Animation State
const pauseOverlayOpacity = useRef(
new Animated.Value(shouldShow ? 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;
// Internal Animation State
const pauseOverlayOpacity = useRef(new Animated.Value(shouldShow ? 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;
// 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;
useEffect(() => {
Animated.timing(pauseOverlayOpacity, {
toValue: shouldShow ? 1 : 0,
duration: 250,
useNativeDriver: true,
}).start();
}, [shouldShow]);
useEffect(() => {
Animated.timing(pauseOverlayOpacity, {
toValue: shouldShow ? 1 : 0,
duration: 250,
useNativeDriver: true
}).start();
}, [shouldShow]);
if (!shouldShow && !showCastDetails) return null;
if (!shouldShow && !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}
/>
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),
}}
>
{t('pause_overlay_screen.back_to_details')}
</Text>
</TouchableOpacity>
<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
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}
>
{t('pause_overlay_screen.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,
}}
>
{t('pause_overlay_screen.you_are_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,
}}
>
{t('pause_overlay_screen.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

@ -1565,13 +1565,19 @@
"later": "Later",
"dismiss": "Dismiss"
},
"major_update_screen":{
"major_update_title":"Major update available",
"latest":"Latest:",
"downloading":"Downloading...",
"update_now":"Update Now",
"later":"Later",
"dismiss":"Dismiss",
"view_release":"View Release"
}
"major_update_screen": {
"major_update_title": "Major update available",
"latest": "Latest:",
"downloading": "Downloading...",
"update_now": "Update Now",
"later": "Later",
"dismiss": "Dismiss",
"view_release": "View Release"
},
"pause_overlay_screen": {
"you_are_watching": "You're watching",
"back_to_details": "Back to Details",
"as":"as",
"cast":"Cast"
}
}

View file

@ -1573,5 +1573,11 @@
"later": "Dopo",
"dismiss": "Ignora",
"view_release": "Vedi Release"
},
"pause_overlay_screen": {
"you_are_watching": "Stai guardando",
"back_to_details": "Torna ai dettagli",
"as": "come",
"cast": "Cast"
}
}