From db9d12491d75edbc9d81fe5399ab96be01d314e0 Mon Sep 17 00:00:00 2001 From: cyberalby2 Date: Fri, 6 Mar 2026 22:45:30 +0100 Subject: [PATCH] Parental Overlay Localization Patch --- .../player/overlays/ParentalGuideOverlay.tsx | 596 ++++++++++-------- src/i18n/locales/en.json | 13 +- src/i18n/locales/it.json | 13 +- 3 files changed, 343 insertions(+), 279 deletions(-) diff --git a/src/components/player/overlays/ParentalGuideOverlay.tsx b/src/components/player/overlays/ParentalGuideOverlay.tsx index 583402e3..8029c308 100644 --- a/src/components/player/overlays/ParentalGuideOverlay.tsx +++ b/src/components/player/overlays/ParentalGuideOverlay.tsx @@ -1,40 +1,41 @@ import React, { useState, useEffect, useRef } from 'react'; import { View, Text, StyleSheet, Dimensions } from 'react-native'; import Animated, { - useSharedValue, - useAnimatedStyle, - withTiming, - withDelay, - Easing, - SharedValue, + useSharedValue, + useAnimatedStyle, + withTiming, + withDelay, + Easing, + SharedValue, } from 'react-native-reanimated'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { parentalGuideService } from '../../../services/parentalGuideService'; import { logger } from '../../../utils/logger'; import { useTheme } from '../../../contexts/ThemeContext'; +import { useTranslation } from 'react-i18next'; interface ParentalGuideOverlayProps { - imdbId: string | undefined; - type: 'movie' | 'series'; - season?: number; - episode?: number; - shouldShow: boolean; + imdbId: string | undefined; + type: 'movie' | 'series'; + season?: number; + episode?: number; + shouldShow: boolean; } interface WarningItem { - label: string; - severity: string; + label: string; + severity: string; } const formatLabel = (key: string): string => { - const labels: Record = { - nudity: 'Nudity', - violence: 'Violence', - profanity: 'Profanity', - alcohol: 'Alcohol/Drugs', - frightening: 'Frightening', - }; - return labels[key] || key; + const labels: Record = { + nudity: 'Nudity', + violence: 'Violence', + profanity: 'Profanity', + alcohol: 'Alcohol/Drugs', + frightening: 'Frightening', + }; + return labels[key] || key; }; // Row height for calculating line animation @@ -42,303 +43,344 @@ const ROW_HEIGHT = 18; // Separate component for each warning item const WarningItemView: React.FC<{ - item: WarningItem; - opacity: SharedValue; - fontSize: number; + item: WarningItem; + opacity: SharedValue; + fontSize: number; }> = ({ item, opacity, fontSize }) => { - const animatedStyle = useAnimatedStyle(() => ({ - opacity: opacity.value, - })); - - return ( - - {item.label} - · - {item.severity} - - ); + const animatedStyle = useAnimatedStyle(() => ({ + opacity: opacity.value, + })); + const { t } = useTranslation(); + return ( + + + {/* replace method added to support "Alcohol/Drugs" label */} + {t(`overlay_screen.${item.label.toLowerCase().replace('/', '_')}`)} + + · + + {t(`overlay_screen.${item.severity.toLowerCase()}`)} + + + ); }; export const ParentalGuideOverlay: React.FC = ({ - imdbId, - type, - season, - episode, - shouldShow, + imdbId, + type, + season, + episode, + shouldShow, }) => { - const { currentTheme } = useTheme(); - const insets = useSafeAreaInsets(); - const screenWidth = Dimensions.get('window').width; - const [warnings, setWarnings] = useState([]); - const [isVisible, setIsVisible] = useState(false); - const hasShownRef = useRef(false); - const hideTimeoutRef = useRef(null); - const fadeTimeoutRef = useRef(null); - const prevShouldShowRef = useRef(false); + const { currentTheme } = useTheme(); + const insets = useSafeAreaInsets(); + const screenWidth = Dimensions.get('window').width; + const [warnings, setWarnings] = useState([]); + const [isVisible, setIsVisible] = useState(false); + const hasShownRef = useRef(false); + const hideTimeoutRef = useRef(null); + const fadeTimeoutRef = useRef(null); + const prevShouldShowRef = useRef(false); - // Animation values - const lineHeight = useSharedValue(0); - const containerOpacity = useSharedValue(0); - const itemOpacity0 = useSharedValue(0); - const itemOpacity1 = useSharedValue(0); - const itemOpacity2 = useSharedValue(0); - const itemOpacity3 = useSharedValue(0); - const itemOpacity4 = useSharedValue(0); + // Animation values + const lineHeight = useSharedValue(0); + const containerOpacity = useSharedValue(0); + const itemOpacity0 = useSharedValue(0); + const itemOpacity1 = useSharedValue(0); + const itemOpacity2 = useSharedValue(0); + const itemOpacity3 = useSharedValue(0); + const itemOpacity4 = useSharedValue(0); - const itemOpacities = [itemOpacity0, itemOpacity1, itemOpacity2, itemOpacity3, itemOpacity4]; + const itemOpacities = [ + itemOpacity0, + itemOpacity1, + itemOpacity2, + itemOpacity3, + itemOpacity4, + ]; - // Fetch parental guide data - useEffect(() => { - const fetchData = async () => { - if (!imdbId) return; + // Fetch parental guide data + useEffect(() => { + const fetchData = async () => { + if (!imdbId) return; - try { - let data; - if (type === 'movie') { - data = await parentalGuideService.getMovieGuide(imdbId); - } else if (type === 'series' && season && episode) { - data = await parentalGuideService.getTVGuide(imdbId, season, episode); - } + try { + let data; + if (type === 'movie') { + data = await parentalGuideService.getMovieGuide(imdbId); + } else if (type === 'series' && season && episode) { + data = await parentalGuideService.getTVGuide(imdbId, season, episode); + } - if (data && data.parentalGuide) { - const guide = data.parentalGuide; - const items: WarningItem[] = []; + if (data && data.parentalGuide) { + const guide = data.parentalGuide; + const items: WarningItem[] = []; - Object.entries(guide).forEach(([key, severity]) => { - if (severity && severity.toLowerCase() !== 'none') { - items.push({ - label: formatLabel(key), - severity: severity, - }); - } - }); + Object.entries(guide).forEach(([key, severity]) => { + if (severity && severity.toLowerCase() !== 'none') { + items.push({ + label: formatLabel(key), + severity: severity, + }); + } + }); - const severityOrder = { severe: 0, moderate: 1, mild: 2, none: 3 }; - items.sort((a, b) => { - const orderA = severityOrder[a.severity.toLowerCase() as keyof typeof severityOrder] ?? 3; - const orderB = severityOrder[b.severity.toLowerCase() as keyof typeof severityOrder] ?? 3; - return orderA - orderB; - }); + const severityOrder = { severe: 0, moderate: 1, mild: 2, none: 3 }; + items.sort((a, b) => { + const orderA = + severityOrder[a.severity.toLowerCase() as keyof typeof severityOrder] ?? + 3; + const orderB = + severityOrder[b.severity.toLowerCase() as keyof typeof severityOrder] ?? + 3; + return orderA - orderB; + }); - setWarnings(items.slice(0, 5)); - logger.log('[ParentalGuideOverlay] Loaded warnings:', items.length); - } - } catch (error) { - logger.error('[ParentalGuideOverlay] Error fetching guide:', error); - } - }; + setWarnings(items.slice(0, 5)); + logger.log('[ParentalGuideOverlay] Loaded warnings:', items.length); + } + } catch (error) { + logger.error('[ParentalGuideOverlay] Error fetching guide:', error); + } + }; - fetchData(); - }, [imdbId, type, season, episode]); + fetchData(); + }, [imdbId, type, season, episode]); - // Handle show/hide based on shouldShow (controls visibility) - useEffect(() => { - // When controls are shown (shouldShow becomes false), immediately hide overlay - if (!shouldShow && isVisible) { - // Clear any pending timeouts - if (hideTimeoutRef.current) { - clearTimeout(hideTimeoutRef.current); - hideTimeoutRef.current = null; - } - if (fadeTimeoutRef.current) { - clearTimeout(fadeTimeoutRef.current); - fadeTimeoutRef.current = null; - } + // Handle show/hide based on shouldShow (controls visibility) + useEffect(() => { + // When controls are shown (shouldShow becomes false), immediately hide overlay + if (!shouldShow && isVisible) { + // Clear any pending timeouts + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current); + hideTimeoutRef.current = null; + } + if (fadeTimeoutRef.current) { + clearTimeout(fadeTimeoutRef.current); + fadeTimeoutRef.current = null; + } - // Immediately hide overlay with quick fade out - const count = warnings.length; - // FADE OUT: Items fade out in reverse order (bottom to top) - for (let i = count - 1; i >= 0; i--) { - const reverseDelay = (count - 1 - i) * 40; - itemOpacities[i].value = withDelay( - reverseDelay, - withTiming(0, { duration: 100 }) - ); - } + // Immediately hide overlay with quick fade out + const count = warnings.length; + // FADE OUT: Items fade out in reverse order (bottom to top) + for (let i = count - 1; i >= 0; i--) { + const reverseDelay = (count - 1 - i) * 40; + itemOpacities[i].value = withDelay( + reverseDelay, + withTiming(0, { duration: 100 }), + ); + } - // Line shrinks after items are gone - const lineDelay = count * 40 + 50; - lineHeight.value = withDelay(lineDelay, withTiming(0, { - duration: 200, - easing: Easing.in(Easing.cubic), - })); + // Line shrinks after items are gone + const lineDelay = count * 40 + 50; + lineHeight.value = withDelay( + lineDelay, + withTiming(0, { + duration: 200, + easing: Easing.in(Easing.cubic), + }), + ); - // Container fades out last - containerOpacity.value = withDelay(lineDelay + 100, withTiming(0, { duration: 150 })); + // Container fades out last + containerOpacity.value = withDelay( + lineDelay + 100, + withTiming(0, { duration: 150 }), + ); - // Set invisible after all animations complete - fadeTimeoutRef.current = setTimeout(() => { - setIsVisible(false); - // Don't reset hasShownRef here - only reset on content change - }, lineDelay + 300); - } - - // When controls are hidden (shouldShow becomes true), show overlay if not already shown for this content - // Only show if transitioning from false to true (controls just hidden) - if (shouldShow && !prevShouldShowRef.current && warnings.length > 0 && !hasShownRef.current) { - hasShownRef.current = true; - setIsVisible(true); + // Set invisible after all animations complete + fadeTimeoutRef.current = setTimeout(() => { + setIsVisible(false); + // Don't reset hasShownRef here - only reset on content change + }, lineDelay + 300); + } - const count = warnings.length; - // Line height = (row height * count) + (gap * (count - 1)) - const gap = 2; // matches styles.itemsContainer gap - const totalLineHeight = (count * ROW_HEIGHT) + ((count - 1) * gap); + // When controls are hidden (shouldShow becomes true), show overlay if not already shown for this content + // Only show if transitioning from false to true (controls just hidden) + if ( + shouldShow && + !prevShouldShowRef.current && + warnings.length > 0 && + !hasShownRef.current + ) { + hasShownRef.current = true; + setIsVisible(true); - // Container fade in - containerOpacity.value = withTiming(1, { duration: 300 }); + const count = warnings.length; + // Line height = (row height * count) + (gap * (count - 1)) + const gap = 2; // matches styles.itemsContainer gap + const totalLineHeight = count * ROW_HEIGHT + (count - 1) * gap; - // FADE IN: Line grows from top to bottom first - lineHeight.value = withTiming(totalLineHeight, { - duration: 400, - easing: Easing.out(Easing.cubic), - }); + // Container fade in + containerOpacity.value = withTiming(1, { duration: 300 }); - // Then each item fades in one by one (after line animation) - for (let i = 0; i < count; i++) { - itemOpacities[i].value = withDelay( - 400 + i * 80, // Start after line, stagger each - withTiming(1, { duration: 200 }) - ); - } + // FADE IN: Line grows from top to bottom first + lineHeight.value = withTiming(totalLineHeight, { + duration: 400, + easing: Easing.out(Easing.cubic), + }); - // Auto-hide after 5 seconds - hideTimeoutRef.current = setTimeout(() => { - // FADE OUT: Items fade out in reverse order (bottom to top) - for (let i = count - 1; i >= 0; i--) { - const reverseDelay = (count - 1 - i) * 60; - itemOpacities[i].value = withDelay( - reverseDelay, - withTiming(0, { duration: 150 }) - ); - } + // Then each item fades in one by one (after line animation) + for (let i = 0; i < count; i++) { + itemOpacities[i].value = withDelay( + 400 + i * 80, // Start after line, stagger each + withTiming(1, { duration: 200 }), + ); + } - // Line shrinks after items are gone - const lineDelay = count * 60 + 100; - lineHeight.value = withDelay(lineDelay, withTiming(0, { - duration: 300, - easing: Easing.in(Easing.cubic), - })); + // Auto-hide after 5 seconds + hideTimeoutRef.current = setTimeout(() => { + // FADE OUT: Items fade out in reverse order (bottom to top) + for (let i = count - 1; i >= 0; i--) { + const reverseDelay = (count - 1 - i) * 60; + itemOpacities[i].value = withDelay( + reverseDelay, + withTiming(0, { duration: 150 }), + ); + } - // Container fades out last - containerOpacity.value = withDelay(lineDelay + 200, withTiming(0, { duration: 200 })); + // Line shrinks after items are gone + const lineDelay = count * 60 + 100; + lineHeight.value = withDelay( + lineDelay, + withTiming(0, { + duration: 300, + easing: Easing.in(Easing.cubic), + }), + ); - // Set invisible after all animations complete - fadeTimeoutRef.current = setTimeout(() => { - setIsVisible(false); - // Don't reset hasShownRef - only reset on content change - }, lineDelay + 500); - }, 5000); - } + // Container fades out last + containerOpacity.value = withDelay( + lineDelay + 200, + withTiming(0, { duration: 200 }), + ); - // Update previous shouldShow value - prevShouldShowRef.current = shouldShow; - }, [shouldShow, isVisible, warnings.length]); + // Set invisible after all animations complete + fadeTimeoutRef.current = setTimeout(() => { + setIsVisible(false); + // Don't reset hasShownRef - only reset on content change + }, lineDelay + 500); + }, 5000); + } - // Cleanup on unmount - useEffect(() => { - return () => { - if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current); - if (fadeTimeoutRef.current) clearTimeout(fadeTimeoutRef.current); - }; - }, []); + // Update previous shouldShow value + prevShouldShowRef.current = shouldShow; + }, [shouldShow, isVisible, warnings.length]); - // Reset when content changes - useEffect(() => { - hasShownRef.current = false; - prevShouldShowRef.current = false; - setWarnings([]); - setIsVisible(false); - lineHeight.value = 0; - containerOpacity.value = 0; - for (let i = 0; i < 5; i++) { - itemOpacities[i].value = 0; - } + // Cleanup on unmount + useEffect(() => { + return () => { + if (hideTimeoutRef.current) clearTimeout(hideTimeoutRef.current); + if (fadeTimeoutRef.current) clearTimeout(fadeTimeoutRef.current); + }; + }, []); - if (hideTimeoutRef.current) { - clearTimeout(hideTimeoutRef.current); - hideTimeoutRef.current = null; - } - if (fadeTimeoutRef.current) { - clearTimeout(fadeTimeoutRef.current); - fadeTimeoutRef.current = null; - } - }, [imdbId, season, episode]); + // Reset when content changes + useEffect(() => { + hasShownRef.current = false; + prevShouldShowRef.current = false; + setWarnings([]); + setIsVisible(false); + lineHeight.value = 0; + containerOpacity.value = 0; + for (let i = 0; i < 5; i++) { + itemOpacities[i].value = 0; + } - const containerStyle = useAnimatedStyle(() => ({ - opacity: containerOpacity.value, - })); + if (hideTimeoutRef.current) { + clearTimeout(hideTimeoutRef.current); + hideTimeoutRef.current = null; + } + if (fadeTimeoutRef.current) { + clearTimeout(fadeTimeoutRef.current); + fadeTimeoutRef.current = null; + } + }, [imdbId, season, episode]); - const lineStyle = useAnimatedStyle(() => ({ - height: lineHeight.value, - })); + const containerStyle = useAnimatedStyle(() => ({ + opacity: containerOpacity.value, + })); - if (!isVisible || warnings.length === 0) { - return null; - } + const lineStyle = useAnimatedStyle(() => ({ + height: lineHeight.value, + })); - // 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); + if (!isVisible || warnings.length === 0) { + return null; + } - // Use left inset for landscape notches, top inset for portrait - const safeLeftOffset = insets.left + containerPadding; - const safeTopOffset = containerPadding; + // 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 ( - - {/* Vertical line - animates height */} - + // Use left inset for landscape notches, top inset for portrait + const safeLeftOffset = insets.left + containerPadding; + const safeTopOffset = containerPadding; - {/* Warning items */} - - {warnings.map((item, index) => ( - - ))} - - - ); + return ( + + {/* Vertical line - animates height */} + + + {/* Warning items */} + + {warnings.map((item, index) => ( + + ))} + + + ); }; const styles = StyleSheet.create({ - container: { - position: 'absolute', - flexDirection: 'row', - alignItems: 'flex-start', - zIndex: 100, - }, - line: { - borderRadius: 1, - marginRight: 10, - }, - itemsContainer: { - gap: 2, - }, - warningItem: { - flexDirection: 'row', - alignItems: 'center', - height: ROW_HEIGHT, - }, - label: { - color: 'rgba(255, 255, 255, 0.85)', - fontSize: 11, - fontWeight: '600', - }, - separator: { - color: 'rgba(255, 255, 255, 0.4)', - fontSize: 11, - marginHorizontal: 5, - }, - severity: { - color: 'rgba(255, 255, 255, 0.5)', - fontSize: 11, - fontWeight: '400', - }, + container: { + position: 'absolute', + flexDirection: 'row', + alignItems: 'flex-start', + zIndex: 100, + }, + line: { + borderRadius: 1, + marginRight: 10, + }, + itemsContainer: { + gap: 2, + }, + warningItem: { + flexDirection: 'row', + alignItems: 'center', + height: ROW_HEIGHT, + }, + label: { + color: 'rgba(255, 255, 255, 0.85)', + fontSize: 11, + fontWeight: '600', + }, + separator: { + color: 'rgba(255, 255, 255, 0.4)', + fontSize: 11, + marginHorizontal: 5, + }, + severity: { + color: 'rgba(255, 255, 255, 0.5)', + fontSize: 11, + fontWeight: '400', + }, }); export default ParentalGuideOverlay; diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 1bae5e1f..0c1713dc 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1531,6 +1531,17 @@ "library_desc": "Save favorites, track your progress, and sync with Trakt to keep everything organized across devices.", "swipe_to_continue": "Swipe to continue", "skip": "Skip", - "get_started":"Get Started" + "get_started": "Get Started" + }, + "overlay_screen": { + "nudity": "Nudity", + "violence": "Violence", + "profanity": "Profanity", + "alcohol": "Alcohol/Drugs", + "frightening": "Frightening", + "severe":"severe", + "moderate":"moderate", + "mild":"mild", + "none":"none" } } diff --git a/src/i18n/locales/it.json b/src/i18n/locales/it.json index 2bc7254e..6c206d59 100644 --- a/src/i18n/locales/it.json +++ b/src/i18n/locales/it.json @@ -1389,7 +1389,7 @@ "theme": { "title": "Temi App", "select_theme": "SELEZIONA TEMA", - "create_custom": "Crea Tema Personalizado", + "create_custom": "Crea Tema Personalizzato", "options": "OPZIONI", "use_dominant_color": "Usa colore dominante dall'artwork", "categories": { @@ -1532,5 +1532,16 @@ "swipe_to_continue": "Scorri per proseguire", "skip": "Salta", "get_started": "Inizia" + }, + "overlay_screen": { + "nudity": "Nudità", + "violence": "Violenza", + "profanity": "Volgarità", + "alcohol_drugs": "Alcol/Droga", + "frightening": "Paura", + "severe": "severa", + "moderate": "moderata", + "mild": "lieve", + "none": "nessuno" } }