updated remaining main screens for localization

This commit is contained in:
tapframe 2026-01-06 14:04:16 +05:30
parent cdab715463
commit 6ef047db3c
15 changed files with 519 additions and 157 deletions

View file

@ -12,6 +12,7 @@ import {
Image, Image,
} from 'react-native'; } from 'react-native';
import { NavigationProp, useNavigation, useIsFocused } from '@react-navigation/native'; import { NavigationProp, useNavigation, useIsFocused } from '@react-navigation/native';
import { useTranslation } from 'react-i18next';
import { RootStackParamList } from '../../navigation/AppNavigator'; import { RootStackParamList } from '../../navigation/AppNavigator';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import FastImage from '@d11/react-native-fast-image'; import FastImage from '@d11/react-native-fast-image';
@ -144,6 +145,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
onRetry, onRetry,
scrollY: externalScrollY, scrollY: externalScrollY,
}) => { }) => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const isFocused = useIsFocused(); const isFocused = useIsFocused();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
@ -158,7 +160,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
const [inLibrary, setInLibrary] = useState(false); const [inLibrary, setInLibrary] = useState(false);
const [isInWatchlist, setIsInWatchlist] = useState(false); const [isInWatchlist, setIsInWatchlist] = useState(false);
const [isWatched, setIsWatched] = useState(false); const [isWatched, setIsWatched] = useState(false);
const [playButtonText, setPlayButtonText] = useState('Play'); const [shouldResume, setShouldResume] = useState(false);
const [type, setType] = useState<'movie' | 'series'>('movie'); const [type, setType] = useState<'movie' | 'series'>('movie');
// Create internal scrollY if not provided externally // Create internal scrollY if not provided externally
@ -530,7 +532,8 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
useEffect(() => { useEffect(() => {
if (currentItem) { if (currentItem) {
const buttonText = getProgressPlayButtonText(); const buttonText = getProgressPlayButtonText();
setPlayButtonText(buttonText); // Use internal state for resume logic instead of string comparison
setShouldResume(buttonText === 'Resume');
// Update watched state based on progress // Update watched state based on progress
if (watchProgress) { if (watchProgress) {
@ -987,10 +990,10 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
<View style={[styles.container, { height: HERO_HEIGHT, marginTop: -insets.top }]}> <View style={[styles.container, { height: HERO_HEIGHT, marginTop: -insets.top }]}>
<View style={styles.noContentContainer}> <View style={styles.noContentContainer}>
<MaterialIcons name="theaters" size={48} color="rgba(255,255,255,0.5)" /> <MaterialIcons name="theaters" size={48} color="rgba(255,255,255,0.5)" />
<Text style={styles.noContentText}>No featured content available</Text> <Text style={styles.noContentText}>{t('home.no_featured_available')}</Text>
{onRetry && ( {onRetry && (
<TouchableOpacity style={styles.retryButton} onPress={onRetry} activeOpacity={0.7}> <TouchableOpacity style={styles.retryButton} onPress={onRetry} activeOpacity={0.7}>
<Text style={styles.retryButtonText}>Retry</Text> <Text style={styles.retryButtonText}>{t('home.retry')}</Text>
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
@ -1242,7 +1245,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
<View style={styles.metadataBadge}> <View style={styles.metadataBadge}>
<MaterialIcons name="tv" size={16} color="#fff" /> <MaterialIcons name="tv" size={16} color="#fff" />
<Text style={styles.metadataText}> <Text style={styles.metadataText}>
{currentItem.type === 'series' ? 'TV Show' : 'Movie'} {currentItem.type === 'series' ? t('home.tv_show') : t('home.movie')}
</Text> </Text>
{currentItem.genres && currentItem.genres.length > 0 && ( {currentItem.genres && currentItem.genres.length > 0 && (
<> <>
@ -1262,11 +1265,11 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
activeOpacity={0.85} activeOpacity={0.85}
> >
<MaterialIcons <MaterialIcons
name={playButtonText === 'Resume' ? "replay" : "play-arrow"} name={shouldResume ? "replay" : "play-arrow"}
size={24} size={24}
color="#000" color="#000"
/> />
<Text style={styles.playButtonText}>{playButtonText}</Text> <Text style={styles.playButtonText}>{shouldResume ? t('home.resume') : t('home.play')}</Text>
</TouchableOpacity> </TouchableOpacity>
{/* Save Button */} {/* Save Button */}

View file

@ -1,6 +1,7 @@
import React, { useCallback, useMemo, useRef } from 'react'; import React, { useCallback, useMemo, useRef } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, Platform, Dimensions, FlatList } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity, Platform, Dimensions, FlatList } from 'react-native';
import { NavigationProp, useNavigation } from '@react-navigation/native'; import { NavigationProp, useNavigation } from '@react-navigation/native';
import { useTranslation } from 'react-i18next';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { CatalogContent, StreamingContent } from '../../services/catalogService'; import { CatalogContent, StreamingContent } from '../../services/catalogService';
@ -73,6 +74,7 @@ const posterLayout = calculatePosterLayout(width);
const POSTER_WIDTH = posterLayout.posterWidth; const POSTER_WIDTH = posterLayout.posterWidth;
const CatalogSection = ({ catalog }: CatalogSectionProps) => { const CatalogSection = ({ catalog }: CatalogSectionProps) => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
@ -154,7 +156,7 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14, fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14,
marginRight: isTV ? 6 : isLargeTablet ? 5 : 4, marginRight: isTV ? 6 : isLargeTablet ? 5 : 4,
} }
]}>View All</Text> ]}>{t('home.view_all')}</Text>
<MaterialIcons <MaterialIcons
name="chevron-right" name="chevron-right"
size={isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20} size={isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20}

View file

@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback, useRef } from 'react'; import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useToast } from '../../contexts/ToastContext'; import { useToast } from '../../contexts/ToastContext';
import { DeviceEventEmitter } from 'react-native'; import { DeviceEventEmitter } from 'react-native';
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform, Text, Share } from 'react-native'; import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform, Text, Share } from 'react-native';
@ -82,6 +83,7 @@ const posterLayout = calculatePosterLayout(width);
const POSTER_WIDTH = posterLayout.posterWidth; const POSTER_WIDTH = posterLayout.posterWidth;
const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, deferMs = 0 }: ContentItemProps) => { const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, deferMs = 0 }: ContentItemProps) => {
const { t } = useTranslation();
// Track inLibrary status locally to force re-render // Track inLibrary status locally to force re-render
const [inLibrary, setInLibrary] = useState(!!item.inLibrary); const [inLibrary, setInLibrary] = useState(!!item.inLibrary);
@ -182,10 +184,10 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
case 'library': case 'library':
if (inLibrary) { if (inLibrary) {
catalogService.removeFromLibrary(item.type, item.id); catalogService.removeFromLibrary(item.type, item.id);
showInfo('Removed from Library', 'Removed from your local library'); showInfo(t('library.removed_from_library'), t('library.item_removed'));
} else { } else {
catalogService.addToLibrary(item); catalogService.addToLibrary(item);
showSuccess('Added to Library', 'Added to your local library'); showSuccess(t('library.added_to_library'), t('library.item_added'));
} }
break; break;
case 'watched': { case 'watched': {
@ -194,7 +196,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
try { try {
await mmkvStorage.setItem(`watched:${item.type}:${item.id}`, targetWatched ? 'true' : 'false'); await mmkvStorage.setItem(`watched:${item.type}:${item.id}`, targetWatched ? 'true' : 'false');
} catch { } } catch { }
showInfo(targetWatched ? 'Marked as Watched' : 'Marked as Unwatched', targetWatched ? 'Item marked as watched' : 'Item marked as unwatched'); showInfo(targetWatched ? t('library.marked_watched') : t('library.marked_unwatched'), targetWatched ? t('library.item_marked_watched') : t('library.item_marked_unwatched'));
setTimeout(() => { setTimeout(() => {
DeviceEventEmitter.emit('watchedStatusChanged'); DeviceEventEmitter.emit('watchedStatusChanged');
}, 100); }, 100);
@ -240,10 +242,10 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
case 'trakt-watchlist': { case 'trakt-watchlist': {
if (isInWatchlist(item.id, item.type as 'movie' | 'show')) { if (isInWatchlist(item.id, item.type as 'movie' | 'show')) {
await removeFromWatchlist(item.id, item.type as 'movie' | 'show'); await removeFromWatchlist(item.id, item.type as 'movie' | 'show');
showInfo('Removed from Watchlist', 'Removed from your Trakt watchlist'); showInfo(t('library.removed_from_watchlist'), t('library.removed_from_watchlist_desc'));
} else { } else {
await addToWatchlist(item.id, item.type as 'movie' | 'show'); await addToWatchlist(item.id, item.type as 'movie' | 'show');
showSuccess('Added to Watchlist', 'Added to your Trakt watchlist'); showSuccess(t('library.added_to_watchlist'), t('library.added_to_watchlist_desc'));
} }
setMenuVisible(false); setMenuVisible(false);
break; break;
@ -251,10 +253,10 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
case 'trakt-collection': { case 'trakt-collection': {
if (isInCollection(item.id, item.type as 'movie' | 'show')) { if (isInCollection(item.id, item.type as 'movie' | 'show')) {
await removeFromCollection(item.id, item.type as 'movie' | 'show'); await removeFromCollection(item.id, item.type as 'movie' | 'show');
showInfo('Removed from Collection', 'Removed from your Trakt collection'); showInfo(t('library.removed_from_collection'), t('library.removed_from_collection_desc'));
} else { } else {
await addToCollection(item.id, item.type as 'movie' | 'show'); await addToCollection(item.id, item.type as 'movie' | 'show');
showSuccess('Added to Collection', 'Added to your Trakt collection'); showSuccess(t('library.added_to_collection'), t('library.added_to_collection_desc'));
} }
setMenuVisible(false); setMenuVisible(false);
break; break;

View file

@ -11,6 +11,7 @@ import {
Platform Platform
} from 'react-native'; } from 'react-native';
import { FlatList } from 'react-native'; import { FlatList } from 'react-native';
import { useTranslation } from 'react-i18next';
import Animated, { FadeIn, Layout } from 'react-native-reanimated'; import Animated, { FadeIn, Layout } from 'react-native-reanimated';
import BottomSheet, { BottomSheetModal, BottomSheetView, BottomSheetBackdrop } from '@gorhom/bottom-sheet'; import BottomSheet, { BottomSheetModal, BottomSheetView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from '@expo/vector-icons';
@ -107,6 +108,7 @@ const isEpisodeReleased = (video: any): boolean => {
// Create a proper imperative handle with React.forwardRef and updated type // Create a proper imperative handle with React.forwardRef and updated type
const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, ref) => { const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, ref) => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { settings } = useSettings(); const { settings } = useSettings();
@ -1310,7 +1312,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
{/* Up Next Badge */} {/* Up Next Badge */}
{item.type === 'series' && item.progress === 0 && ( {item.type === 'series' && item.progress === 0 && (
<View style={[styles.posterUpNextBadge, { backgroundColor: currentTheme.colors.primary }]}> <View style={[styles.posterUpNextBadge, { backgroundColor: currentTheme.colors.primary }]}>
<Text style={[styles.posterUpNextText, { fontSize: isTV ? 12 : 10 }]}>UP NEXT</Text> <Text style={[styles.posterUpNextText, { fontSize: isTV ? 12 : 10 }]}>{t('home.up_next_caps')}</Text>
</View> </View>
)} )}
@ -1441,7 +1443,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
<Text style={[ <Text style={[
styles.progressText, styles.progressText,
{ fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12 } { fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12 }
]}>Up Next</Text> ]}>{t('home.up_next')}</Text>
</View> </View>
)} )}
</View> </View>
@ -1460,7 +1462,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 13 fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 13
} }
]}> ]}>
Season {item.season} {t('home.season', { season: item.season })}
</Text> </Text>
{item.episodeTitle && ( {item.episodeTitle && (
<Text <Text
@ -1487,7 +1489,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 13 fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 13
} }
]}> ]}>
{item.year} {item.type === 'movie' ? 'Movie' : 'Series'} {item.year} {item.type === 'movie' ? t('home.movie') : t('home.series')}
</Text> </Text>
); );
} }
@ -1519,7 +1521,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 11 fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 11
} }
]}> ]}>
{Math.round(item.progress)}% watched {t('home.percent_watched', { percent: Math.round(item.progress) })}
</Text> </Text>
</View> </View>
)} )}
@ -1558,7 +1560,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
color: currentTheme.colors.text, color: currentTheme.colors.text,
fontSize: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 26 : 24 fontSize: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 26 : 24
} }
]}>Continue Watching</Text> ]}>{t('home.continue_watching')}</Text>
<View style={[ <View style={[
styles.titleUnderline, styles.titleUnderline,
{ {
@ -1631,12 +1633,12 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
</Text> </Text>
{selectedItem.type === 'series' && selectedItem.season && selectedItem.episode ? ( {selectedItem.type === 'series' && selectedItem.season && selectedItem.episode ? (
<Text style={[styles.actionSheetSubtitle, { color: currentTheme.colors.textMuted }]}> <Text style={[styles.actionSheetSubtitle, { color: currentTheme.colors.textMuted }]}>
Season {selectedItem.season} · Episode {selectedItem.episode} {t('home.season', { season: selectedItem.season })} · {t('home.episode', { episode: selectedItem.episode })}
{selectedItem.episodeTitle && selectedItem.episodeTitle !== `Episode ${selectedItem.episode}` && `\n${selectedItem.episodeTitle}`} {selectedItem.episodeTitle && selectedItem.episodeTitle !== `Episode ${selectedItem.episode}` && `\n${selectedItem.episodeTitle}`}
</Text> </Text>
) : ( ) : (
<Text style={[styles.actionSheetSubtitle, { color: currentTheme.colors.textMuted }]}> <Text style={[styles.actionSheetSubtitle, { color: currentTheme.colors.textMuted }]}>
{selectedItem.year ? `${selectedItem.type === 'movie' ? 'Movie' : 'Series'} · ${selectedItem.year}` : selectedItem.type === 'movie' ? 'Movie' : 'Series'} {selectedItem.year ? `${selectedItem.type === 'movie' ? t('home.movie') : t('home.series')} · ${selectedItem.year}` : selectedItem.type === 'movie' ? t('home.movie') : t('home.series')}
</Text> </Text>
)} )}
{selectedItem.progress > 0 && ( {selectedItem.progress > 0 && (
@ -1653,7 +1655,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
/> />
</View> </View>
<Text style={[styles.actionSheetProgressText, { color: currentTheme.colors.textMuted }]}> <Text style={[styles.actionSheetProgressText, { color: currentTheme.colors.textMuted }]}>
{Math.round(selectedItem.progress)}% watched {t('home.percent_watched', { percent: Math.round(selectedItem.progress) })}
</Text> </Text>
</View> </View>
)} )}
@ -1668,7 +1670,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
activeOpacity={0.8} activeOpacity={0.8}
> >
<Ionicons name="information-circle-outline" size={22} color="#fff" /> <Ionicons name="information-circle-outline" size={22} color="#fff" />
<Text style={styles.actionButtonText}>View Details</Text> <Text style={styles.actionButtonText}>{t('home.view_details')}</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
@ -1677,7 +1679,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
activeOpacity={0.8} activeOpacity={0.8}
> >
<Ionicons name="trash-outline" size={22} color={currentTheme.colors.error} /> <Ionicons name="trash-outline" size={22} color={currentTheme.colors.error} />
<Text style={[styles.actionButtonText, { color: currentTheme.colors.error }]}>Remove</Text> <Text style={[styles.actionButtonText, { color: currentTheme.colors.error }]}>{t('home.remove')}</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
</> </>

View file

@ -10,6 +10,7 @@ import {
Dimensions, Dimensions,
Platform Platform
} from 'react-native'; } from 'react-native';
import { useTranslation } from 'react-i18next';
import { MaterialIcons } from '@expo/vector-icons'; import { MaterialIcons } from '@expo/vector-icons';
import FastImage from '@d11/react-native-fast-image'; import FastImage from '@d11/react-native-fast-image';
import { useTraktContext } from '../../contexts/TraktContext'; import { useTraktContext } from '../../contexts/TraktContext';
@ -39,6 +40,7 @@ interface DropUpMenuProps {
} }
export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: isSavedProp, isWatched: isWatchedProp }: DropUpMenuProps) => { export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: isSavedProp, isWatched: isWatchedProp }: DropUpMenuProps) => {
const { t } = useTranslation();
const translateY = useSharedValue(300); const translateY = useSharedValue(300);
const opacity = useSharedValue(0); const opacity = useSharedValue(0);
const isDarkMode = useColorScheme() === 'dark'; const isDarkMode = useColorScheme() === 'dark';
@ -102,12 +104,12 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
let menuOptions = [ let menuOptions = [
{ {
icon: 'bookmark', icon: 'bookmark',
label: isSaved ? 'Remove from Library' : 'Add to Library', label: isSaved ? t('library.remove_from_library') : t('library.add_to_library'),
action: 'library' action: 'library'
}, },
{ {
icon: 'check-circle', icon: 'check-circle',
label: isWatched ? 'Mark as Unwatched' : 'Mark as Watched', label: isWatched ? t('library.mark_unwatched') : t('library.mark_watched'),
action: 'watched' action: 'watched'
}, },
/* /*
@ -119,7 +121,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
*/ */
{ {
icon: 'share', icon: 'share',
label: 'Share', label: t('library.share'),
action: 'share' action: 'share'
} }
]; ];
@ -129,12 +131,12 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
menuOptions.push( menuOptions.push(
{ {
icon: 'playlist-add-check', icon: 'playlist-add-check',
label: inTraktWatchlist ? 'Remove from Trakt Watchlist' : 'Add to Trakt Watchlist', label: inTraktWatchlist ? t('library.remove_from_watchlist') : t('library.add_to_watchlist'),
action: 'trakt-watchlist' action: 'trakt-watchlist'
}, },
{ {
icon: 'video-library', icon: 'video-library',
label: inTraktCollection ? 'Remove from Trakt Collection' : 'Add to Trakt Collection', label: inTraktCollection ? t('library.remove_from_collection') : t('library.add_to_collection'),
action: 'trakt-collection' action: 'trakt-collection'
} }
); );

View file

@ -13,6 +13,7 @@ import {
Platform Platform
} from 'react-native'; } from 'react-native';
import { NavigationProp, useNavigation } from '@react-navigation/native'; import { NavigationProp, useNavigation } from '@react-navigation/native';
import { useTranslation } from 'react-i18next';
import { RootStackParamList } from '../../navigation/AppNavigator'; import { RootStackParamList } from '../../navigation/AppNavigator';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import FastImage from '@d11/react-native-fast-image'; import FastImage from '@d11/react-native-fast-image';
@ -52,6 +53,7 @@ const nowMs = () => Date.now();
const since = (start: number) => `${(nowMs() - start).toFixed(0)}ms`; const since = (start: number) => `${(nowMs() - start).toFixed(0)}ms`;
const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => { const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
@ -103,11 +105,11 @@ const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => {
return ( return (
<View style={styles.noContentContainer}> <View style={styles.noContentContainer}>
<MaterialIcons name="theaters" size={48} color={currentTheme.colors.mediumEmphasis} /> <MaterialIcons name="theaters" size={48} color={currentTheme.colors.mediumEmphasis} />
<Text style={styles.noContentTitle}>{onRetry ? 'Couldn\'t load featured content' : 'No Featured Content'}</Text> <Text style={styles.noContentTitle}>{onRetry ? t('home.couldnt_load_featured') : t('home.no_featured_content')}</Text>
<Text style={styles.noContentText}> <Text style={styles.noContentText}>
{onRetry {onRetry
? 'There was a problem fetching featured content. Please check your connection and try again.' ? t('home.load_error_desc')
: 'Install addons with catalogs or change the content source in your settings.'} : t('home.no_featured_desc')}
</Text> </Text>
<View style={styles.noContentButtons}> <View style={styles.noContentButtons}>
{onRetry ? ( {onRetry ? (
@ -115,7 +117,7 @@ const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => {
style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]} style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={onRetry} onPress={onRetry}
> >
<Text style={[styles.noContentButtonText, { color: currentTheme.colors.white }]}>Retry</Text> <Text style={[styles.noContentButtonText, { color: currentTheme.colors.white }]}>{t('home.retry')}</Text>
</TouchableOpacity> </TouchableOpacity>
) : ( ) : (
<> <>
@ -123,13 +125,13 @@ const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => {
style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]} style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={() => navigation.navigate('Addons')} onPress={() => navigation.navigate('Addons')}
> >
<Text style={[styles.noContentButtonText, { color: currentTheme.colors.white }]}>Install Addons</Text> <Text style={[styles.noContentButtonText, { color: currentTheme.colors.white }]}>{t('home.install_addons')}</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={styles.noContentButton} style={styles.noContentButton}
onPress={() => navigation.navigate('HomeScreenSettings')} onPress={() => navigation.navigate('HomeScreenSettings')}
> >
<Text style={styles.noContentButtonText}>Settings</Text> <Text style={styles.noContentButtonText}>{t('home.settings')}</Text>
</TouchableOpacity> </TouchableOpacity>
</> </>
)} )}
@ -139,6 +141,7 @@ const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => {
}; };
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loading, onRetry }: FeaturedContentProps) => { const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loading, onRetry }: FeaturedContentProps) => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const [bannerUrl, setBannerUrl] = useState<string | null>(null); const [bannerUrl, setBannerUrl] = useState<string | null>(null);
@ -509,7 +512,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
> >
<MaterialIcons name="play-arrow" size={28} color={currentTheme.colors.black} /> <MaterialIcons name="play-arrow" size={28} color={currentTheme.colors.black} />
<Text style={[styles.tabletPlayButtonText as TextStyle, { color: currentTheme.colors.black }]}> <Text style={[styles.tabletPlayButtonText as TextStyle, { color: currentTheme.colors.black }]}>
Play Now {t('home.play_now')}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
@ -520,7 +523,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
> >
<MaterialIcons name={isSaved ? "bookmark" : "bookmark-outline"} size={20} color={currentTheme.colors.white} /> <MaterialIcons name={isSaved ? "bookmark" : "bookmark-outline"} size={20} color={currentTheme.colors.white} />
<Text style={[styles.tabletSecondaryButtonText as TextStyle, { color: currentTheme.colors.white }]}> <Text style={[styles.tabletSecondaryButtonText as TextStyle, { color: currentTheme.colors.white }]}>
{isSaved ? "Saved" : "My List"} {isSaved ? t('home.saved') : t('home.my_list')}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
@ -531,7 +534,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
> >
<MaterialIcons name="info-outline" size={20} color={currentTheme.colors.white} /> <MaterialIcons name="info-outline" size={20} color={currentTheme.colors.white} />
<Text style={[styles.tabletSecondaryButtonText as TextStyle, { color: currentTheme.colors.white }]}> <Text style={[styles.tabletSecondaryButtonText as TextStyle, { color: currentTheme.colors.white }]}>
More Info {t('home.more_info')}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</Animated.View> </Animated.View>
@ -626,7 +629,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
> >
<MaterialIcons name={isSaved ? "bookmark" : "bookmark-outline"} size={24} color={currentTheme.colors.white} /> <MaterialIcons name={isSaved ? "bookmark" : "bookmark-outline"} size={24} color={currentTheme.colors.white} />
<Text style={[styles.myListButtonText as TextStyle, { color: currentTheme.colors.white }]}> <Text style={[styles.myListButtonText as TextStyle, { color: currentTheme.colors.white }]}>
{isSaved ? "Saved" : "Save"} {isSaved ? t('home.saved') : t('home.save')}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
@ -644,7 +647,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
> >
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} /> <MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}> <Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
Play {t('home.play')}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
@ -655,7 +658,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
> >
<MaterialIcons name="info-outline" size={24} color={currentTheme.colors.white} /> <MaterialIcons name="info-outline" size={24} color={currentTheme.colors.white} />
<Text style={[styles.infoButtonText as TextStyle, { color: currentTheme.colors.white }]}> <Text style={[styles.infoButtonText as TextStyle, { color: currentTheme.colors.white }]}>
Info {t('home.info')}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</Animated.View> </Animated.View>

View file

@ -1,5 +1,6 @@
import React, { useMemo, useState, useEffect, useCallback, memo, useRef } from 'react'; import React, { useMemo, useState, useEffect, useCallback, memo, useRef } from 'react';
import { View, Text, StyleSheet, TouchableOpacity, ViewStyle, TextStyle, ImageStyle, ScrollView, StyleProp, Platform, Image, useWindowDimensions } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity, ViewStyle, TextStyle, ImageStyle, ScrollView, StyleProp, Platform, Image, useWindowDimensions } from 'react-native';
import { useTranslation } from 'react-i18next';
import Animated, { FadeIn, FadeOut, Easing, useSharedValue, withTiming, useAnimatedStyle, useAnimatedScrollHandler, useAnimatedReaction, runOnJS, SharedValue, interpolate, Extrapolation } from 'react-native-reanimated'; import Animated, { FadeIn, FadeOut, Easing, useSharedValue, withTiming, useAnimatedStyle, useAnimatedScrollHandler, useAnimatedReaction, runOnJS, SharedValue, interpolate, Extrapolation } from 'react-native-reanimated';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { BlurView } from 'expo-blur'; import { BlurView } from 'expo-blur';
@ -38,6 +39,7 @@ interface HeroCarouselProps {
const TOP_TABS_OFFSET = Platform.OS === 'ios' ? 44 : 48; const TOP_TABS_OFFSET = Platform.OS === 'ios' ? 44 : 48;
const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) => { const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
@ -610,6 +612,7 @@ interface CarouselCardProps {
} }
const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFailed, onLogoError, onPressInfo, scrollX, index, flipped, onToggleFlip, interval, cardWidth, cardHeight, isTablet }) => { const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFailed, onLogoError, onPressInfo, scrollX, index, flipped, onToggleFlip, interval, cardWidth, cardHeight, isTablet }) => {
const { t } = useTranslation();
const [bannerLoaded, setBannerLoaded] = useState(false); const [bannerLoaded, setBannerLoaded] = useState(false);
const [logoLoaded, setLogoLoaded] = useState(false); const [logoLoaded, setLogoLoaded] = useState(false);
@ -847,7 +850,7 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
textShadowRadius: 2, textShadowRadius: 2,
} }
]}> ]}>
{item.description || 'No description available'} {item.description || t('home.no_description')}
</Text> </Text>
</ScrollView> </ScrollView>
</View> </View>
@ -956,7 +959,7 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
textShadowRadius: 2, textShadowRadius: 2,
} }
]}> ]}>
{item.description || 'No description available'} {item.description || t('home.no_description')}
</Text> </Text>
</ScrollView> </ScrollView>
</View> </View>

View file

@ -9,6 +9,7 @@ import {
Dimensions Dimensions
} from 'react-native'; } from 'react-native';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useTranslation } from 'react-i18next';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import FastImage from '@d11/react-native-fast-image'; import FastImage from '@d11/react-native-fast-image';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
@ -58,6 +59,7 @@ interface ThisWeekEpisode {
} }
export const ThisWeekSection = React.memo(() => { export const ThisWeekSection = React.memo(() => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { calendarData, loading } = useCalendarData(); const { calendarData, loading } = useCalendarData();
@ -176,7 +178,7 @@ export const ThisWeekSection = React.memo(() => {
processedItems.push({ processedItems.push({
...firstEp, ...firstEp,
id: `group_${firstEp.seriesId}_${firstEp.releaseDate}`, // Unique ID for the group id: `group_${firstEp.seriesId}_${firstEp.releaseDate}`, // Unique ID for the group
title: `${group.length} New Episodes`, title: t('home.new_episodes', { count: group.length }),
isReleased, isReleased,
isGroup: true, isGroup: true,
episodeCount: group.length, episodeCount: group.length,
@ -239,7 +241,7 @@ export const ThisWeekSection = React.memo(() => {
const renderEpisodeItem = ({ item, index }: { item: ThisWeekEpisode, index: number }) => { const renderEpisodeItem = ({ item, index }: { item: ThisWeekEpisode, index: number }) => {
// Handle episodes without release dates gracefully // Handle episodes without release dates gracefully
const releaseDate = item.releaseDate ? parseISO(item.releaseDate) : null; const releaseDate = item.releaseDate ? parseISO(item.releaseDate) : null;
const formattedDate = releaseDate ? format(releaseDate, 'MMM d') : 'TBA'; const formattedDate = releaseDate ? format(releaseDate, 'MMM d') : t('home.tba');
const isReleased = item.isReleased; const isReleased = item.isReleased;
// Use episode still image if available, fallback to series poster // Use episode still image if available, fallback to series poster
@ -294,12 +296,12 @@ export const ThisWeekSection = React.memo(() => {
locations={[0, 0.4, 0.7, 1]} locations={[0, 0.4, 0.7, 1]}
> >
<View style={styles.cardHeader}> <View style={styles.cardHeader}>
<View style={[ <View style={[
styles.statusBadge, styles.statusBadge,
{ backgroundColor: isReleased ? currentTheme.colors.primary : 'rgba(0,0,0,0.6)' } { backgroundColor: isReleased ? currentTheme.colors.primary : 'rgba(0,0,0,0.6)' }
]}> ]}>
<Text style={styles.statusText}> <Text style={styles.statusText}>
{isReleased ? (item.isGroup ? 'Released' : 'New') : formattedDate} {isReleased ? (item.isGroup ? t('home.released') : t('home.new')) : formattedDate}
</Text> </Text>
</View> </View>
</View> </View>
@ -357,7 +359,7 @@ export const ThisWeekSection = React.memo(() => {
color: currentTheme.colors.text, color: currentTheme.colors.text,
fontSize: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 26 : 24 fontSize: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 26 : 24
} }
]}>This Week</Text> ]}>{t('home.this_week')}</Text>
<View style={[ <View style={[
styles.titleUnderline, styles.titleUnderline,
{ {
@ -380,7 +382,7 @@ export const ThisWeekSection = React.memo(() => {
color: currentTheme.colors.textMuted, color: currentTheme.colors.textMuted,
fontSize: isTV ? 18 : isLargeTablet ? 16 : isTablet ? 15 : 14 fontSize: isTV ? 18 : isLargeTablet ? 16 : isTablet ? 15 : 14
} }
]}>View All</Text> ]}>{t('home.view_all')}</Text>
<MaterialIcons <MaterialIcons
name="chevron-right" name="chevron-right"
size={isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20} size={isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20}

View file

@ -5,20 +5,24 @@ import { mmkvStorage } from '../services/mmkvStorage';
const languageDetector = { const languageDetector = {
type: 'languageDetector', type: 'languageDetector',
async: true, async: true,
detect: async (callback: any) => { detect: (callback?: (lng: string) => void): string | undefined => {
try { const findLanguage = async () => {
const savedLanguage = await mmkvStorage.getItem('user_language'); try {
if (savedLanguage) { const savedLanguage = await mmkvStorage.getItem('user_language');
callback(savedLanguage); if (savedLanguage) {
return; if (callback) callback(savedLanguage);
return;
}
} catch (error) {
console.log('Error reading language from storage', error);
} }
} catch (error) {
console.log('Error reading language from storage', error);
}
const locales = getLocales(); const locales = getLocales();
const languageCode = locales[0]?.languageCode ?? 'en'; const languageCode = locales[0]?.languageCode ?? 'en';
callback(languageCode); if (callback) callback(languageCode);
};
findLanguage();
return undefined;
}, },
init: () => { }, init: () => { },
cacheUserLanguage: (language: string) => { cacheUserLanguage: (language: string) => {

View file

@ -11,6 +11,56 @@
"ok": "OK", "ok": "OK",
"unknown": "Unknown" "unknown": "Unknown"
}, },
"home": {
"categories": {
"movies": "Movies",
"series": "Series",
"channels": "Channels"
},
"movies": "Movies",
"tv_shows": "TV Shows",
"load_more_catalogs": "Load More Catalogs",
"no_content": "No content available",
"add_catalogs": "Add Catalogs",
"sign_in_available": "Sign In Available",
"sign_in_desc": "You can sign in anytime from Settings → Account",
"view_all": "View All",
"this_week": "This Week",
"continue_watching": "Continue Watching",
"up_next": "Up Next",
"up_next_caps": "UP NEXT",
"released": "Released",
"new": "New",
"tba": "TBA",
"new_episodes": "{{count}} New Episodes",
"season_short": "S{{season}}",
"episode_short": "E{{episode}}",
"season": "Season {{season}}",
"episode": "Episode {{episode}}",
"movie": "Movie",
"series": "Series",
"tv_show": "TV Show",
"percent_watched": "{{percent}}% watched",
"view_details": "View Details",
"remove": "Remove",
"play": "Play",
"play_now": "Play Now",
"resume": "Resume",
"info": "Info",
"more_info": "More Info",
"my_list": "My List",
"save": "Save",
"saved": "Saved",
"retry": "Retry",
"install_addons": "Install Addons",
"settings": "Settings",
"no_featured_content": "No Featured Content",
"couldnt_load_featured": "Couldn't load featured content",
"no_featured_desc": "Install addons with catalogs or change the content source in your settings.",
"load_error_desc": "There was a problem fetching featured content. Please check your connection and try again.",
"no_featured_available": "No featured content available",
"no_description": "No description available"
},
"navigation": { "navigation": {
"home": "Home", "home": "Home",
"library": "Library", "library": "Library",
@ -18,6 +68,119 @@
"downloads": "Downloads", "downloads": "Downloads",
"settings": "Settings" "settings": "Settings"
}, },
"search": {
"title": "Search",
"recent_searches": "Recent Searches",
"discover": "Discover",
"movies": "Movies",
"tv_shows": "TV Shows",
"select_catalog": "Select Catalog",
"all_genres": "All Genres",
"discovering": "Discovering content...",
"show_more": "Show More ({{count}})",
"no_content_found": "No content found",
"try_different": "Try a different genre or catalog",
"select_catalog_desc": "Select a catalog to discover",
"tap_catalog_desc": "Tap the catalog chip above to get started",
"search_placeholder": "Search movies, shows...",
"keep_typing": "Keep typing...",
"type_characters": "Type at least 2 characters to search",
"no_results": "No results found",
"try_keywords": "Try different keywords or check your spelling",
"select_type": "Select Type",
"browse_movies": "Browse movie catalogs",
"browse_tv": "Browse TV series catalogs",
"select_genre": "Select Genre",
"show_all_content": "Show all content",
"genres_count": "{{count}} genres"
},
"library": {
"title": "Library",
"watched": "Watched",
"continue": "Continue",
"watchlist": "Watchlist",
"collection": "Collection",
"rated": "Rated",
"items": "items",
"trakt_collections": "Trakt collections",
"trakt_collection": "Trakt Collection",
"no_trakt": "No Trakt collections",
"no_trakt_desc": "Your Trakt collections will appear here once you start using Trakt",
"load_collections": "Load Collections",
"empty_folder": "No content in {{folder}}",
"empty_folder_desc": "This collection is empty",
"refresh": "Refresh",
"no_movies": "No movies yet",
"no_series": "No TV shows yet",
"no_content": "No content yet",
"add_content_desc": "Add some content to your library to see it here",
"find_something": "Find something to watch",
"removed_from_library": "Removed from Library",
"item_removed": "Item removed from your library",
"failed_update_library": "Failed to update Library",
"unable_remove": "Unable to remove item from library",
"marked_watched": "Marked as Watched",
"marked_unwatched": "Marked as Unwatched",
"item_marked_watched": "Item marked as watched",
"item_marked_unwatched": "Item marked as unwatched",
"failed_update_watched": "Failed to update watched status",
"unable_update_watched": "Unable to update watched status",
"added_to_library": "Added to Library",
"item_added": "Added to your local library",
"add_to_library": "Add to Library",
"remove_from_library": "Remove from Library",
"mark_watched": "Mark as Watched",
"mark_unwatched": "Mark as Unwatched",
"share": "Share",
"add_to_watchlist": "Add to Trakt Watchlist",
"remove_from_watchlist": "Remove from Trakt Watchlist",
"added_to_watchlist": "Added to Watchlist",
"added_to_watchlist_desc": "Added to your Trakt watchlist",
"removed_from_watchlist": "Removed from Watchlist",
"removed_from_watchlist_desc": "Removed from your Trakt watchlist",
"add_to_collection": "Add to Trakt Collection",
"remove_from_collection": "Remove from Trakt Collection",
"added_to_collection": "Added to Collection",
"added_to_collection_desc": "Added to your Trakt collection",
"removed_from_collection": "Removed from Collection",
"removed_from_collection_desc": "Removed from your Trakt collection"
},
"downloads": {
"title": "Downloads",
"no_downloads": "No Downloads Yet",
"no_downloads_desc": "Downloaded content will appear here for offline viewing",
"explore": "Explore Content",
"path_copied": "Path Copied",
"path_copied_desc": "Local file path copied to clipboard",
"copied": "Copied",
"incomplete": "Download Incomplete",
"incomplete_desc": "Download is not complete yet",
"not_available": "Not Available",
"not_available_desc": "The local file path is available only after the download is complete.",
"status_downloading": "Downloading",
"status_completed": "Completed",
"status_paused": "Paused",
"status_error": "Error",
"status_queued": "Queued",
"status_unknown": "Unknown",
"provider": "Provider",
"streaming_playlist_warning": "May not play - streaming playlist",
"remaining": "remaining",
"not_ready": "Download not ready",
"not_ready_desc": "Please wait until the download completes.",
"filter_all": "All",
"filter_active": "Active",
"filter_done": "Done",
"filter_paused": "Paused",
"no_filter_results": "No {{filter}} downloads",
"try_different_filter": "Try selecting a different filter",
"limitations_title": "Download Limitations",
"limitations_msg": "• Files smaller than 1MB are typically M3U8 streaming playlists and cannot be downloaded for offline viewing. These only work with online streaming and contain links to video segments, not the actual video content.",
"remove_title": "Remove Download",
"remove_confirm": "Remove \"{{title}}\"{{season_episode}}?",
"cancel": "Cancel",
"remove": "Remove"
},
"addons": { "addons": {
"title": "Addons", "title": "Addons",
"reorder_mode": "Reorder Mode", "reorder_mode": "Reorder Mode",

View file

@ -11,6 +11,56 @@
"ok": "OK", "ok": "OK",
"unknown": "Desconhecido" "unknown": "Desconhecido"
}, },
"home": {
"categories": {
"movies": "Filmes",
"series": "Séries",
"channels": "Canais"
},
"movies": "Filmes",
"tv_shows": "Séries de TV",
"load_more_catalogs": "Carregar Mais Catálogos",
"no_content": "Nenhum conteúdo disponível",
"add_catalogs": "Adicionar Catálogos",
"sign_in_available": "Entrar Disponível",
"sign_in_desc": "Você pode entrar a qualquer momento em Configurações → Conta",
"view_all": "Ver Tudo",
"this_week": "Esta Semana",
"continue_watching": "Continue Assistindo",
"up_next": "A Seguir",
"up_next_caps": "A SEGUIR",
"released": "Lançado",
"new": "Novo",
"tba": "A confirmar",
"new_episodes": "{{count}} Novos Episódios",
"season_short": "T{{season}}",
"episode_short": "E{{episode}}",
"season": "Temporada {{season}}",
"episode": "Episódio {{episode}}",
"movie": "Filme",
"series": "Série",
"tv_show": "Série de TV",
"percent_watched": "{{percent}}% assistido",
"view_details": "Ver Detalhes",
"remove": "Remover",
"play": "Reproduzir",
"play_now": "Reproduzir Agora",
"resume": "Continuar",
"info": "Info",
"more_info": "Mais Info",
"my_list": "Minha Lista",
"save": "Salvar",
"saved": "Salvo",
"retry": "Tentar Novamente",
"install_addons": "Instalar Addons",
"settings": "Configurações",
"no_featured_content": "Nenhum Conteúdo em Destaque",
"couldnt_load_featured": "Não foi possível carregar o conteúdo em destaque",
"no_featured_desc": "Instale addons com catálogos ou altere a fonte de conteúdo nas configurações.",
"load_error_desc": "Houve um problema ao buscar o conteúdo em destaque. Verifique sua conexão e tente novamente.",
"no_featured_available": "Nenhum conteúdo em destaque disponível",
"no_description": "Nenhuma descrição disponível"
},
"navigation": { "navigation": {
"home": "Início", "home": "Início",
"library": "Biblioteca", "library": "Biblioteca",
@ -18,6 +68,119 @@
"downloads": "Downloads", "downloads": "Downloads",
"settings": "Configurações" "settings": "Configurações"
}, },
"search": {
"title": "Buscar",
"recent_searches": "Buscas Recentes",
"discover": "Descobrir",
"movies": "Filmes",
"tv_shows": "Séries",
"select_catalog": "Selecionar Catálogo",
"all_genres": "Todos os Gêneros",
"discovering": "Descobrindo conteúdo...",
"show_more": "Mostrar Mais ({{count}})",
"no_content_found": "Nenhum conteúdo encontrado",
"try_different": "Tente um gênero ou catálogo diferente",
"select_catalog_desc": "Selecione um catálogo para descobrir",
"tap_catalog_desc": "Toque no botão de catálogo acima para começar",
"search_placeholder": "Buscar filmes, séries...",
"keep_typing": "Continue digitando...",
"type_characters": "Digite pelo menos 2 caracteres para buscar",
"no_results": "Nenhum resultado encontrado",
"try_keywords": "Tente palavras-chave diferentes ou verifique a ortografia",
"select_type": "Selecionar Tipo",
"browse_movies": "Navegar catálogos de filmes",
"browse_tv": "Navegar catálogos de séries",
"select_genre": "Selecionar Gênero",
"show_all_content": "Mostrar todo o conteúdo",
"genres_count": "{{count}} gêneros"
},
"library": {
"title": "Biblioteca",
"watched": "Assistidos",
"continue": "Continuar",
"watchlist": "Lista",
"collection": "Coleção",
"rated": "Avaliado",
"items": "itens",
"trakt_collections": "Coleções Trakt",
"trakt_collection": "Coleção Trakt",
"no_trakt": "Nenhuma coleção Trakt",
"no_trakt_desc": "Suas coleções do Trakt aparecerão aqui quando você começar a usar o Trakt",
"load_collections": "Carregar Coleções",
"empty_folder": "Nenhum conteúdo em {{folder}}",
"empty_folder_desc": "Esta coleção está vazia",
"refresh": "Atualizar",
"no_movies": "Nenhum filme ainda",
"no_series": "Nenhuma série ainda",
"no_content": "Nenhum conteúdo ainda",
"add_content_desc": "Adicione algum conteúdo à sua biblioteca para vê-lo aqui",
"find_something": "Encontrar algo para assistir",
"removed_from_library": "Removido da Biblioteca",
"item_removed": "Item removido da sua biblioteca",
"failed_update_library": "Falha ao atualizar Biblioteca",
"unable_remove": "Não foi possível remover o item da biblioteca",
"marked_watched": "Marcado como Assistido",
"marked_unwatched": "Marcado como Não Assistido",
"item_marked_watched": "Item marcado como assistido",
"item_marked_unwatched": "Item marcado como não assistido",
"failed_update_watched": "Falha ao atualizar status de assistido",
"unable_update_watched": "Não foi possível atualizar o status de assistido",
"added_to_library": "Adicionado à Biblioteca",
"item_added": "Adicionado à sua biblioteca local",
"add_to_library": "Adicionar à Biblioteca",
"remove_from_library": "Remover da Biblioteca",
"mark_watched": "Marcar como Assistido",
"mark_unwatched": "Marcar como Não Assistido",
"share": "Compartilhar",
"add_to_watchlist": "Adicionar à Lista Trakt",
"remove_from_watchlist": "Remover da Lista Trakt",
"added_to_watchlist": "Adicionado à Lista",
"added_to_watchlist_desc": "Adicionado à sua lista Trakt",
"removed_from_watchlist": "Removido da Lista",
"removed_from_watchlist_desc": "Removido da sua lista Trakt",
"add_to_collection": "Adicionar à Coleção Trakt",
"remove_from_collection": "Remover da Coleção Trakt",
"added_to_collection": "Adicionado à Coleção",
"added_to_collection_desc": "Adicionado à sua coleção Trakt",
"removed_from_collection": "Removido da Coleção",
"removed_from_collection_desc": "Removido da sua coleção Trakt"
},
"downloads": {
"title": "Downloads",
"no_downloads": "Nenhum Download Ainda",
"no_downloads_desc": "Conteúdo baixado aparecerá aqui para visualização offline",
"explore": "Explorar Conteúdo",
"path_copied": "Caminho Copiado",
"path_copied_desc": "Caminho do arquivo local copiado para a área de transferência",
"copied": "Copiado",
"incomplete": "Download Incompleto",
"incomplete_desc": "O download ainda não está completo",
"not_available": "Não Disponível",
"not_available_desc": "O caminho do arquivo local está disponível apenas após a conclusão do download.",
"status_downloading": "Baixando",
"status_completed": "Concluído",
"status_paused": "Pausado",
"status_error": "Erro",
"status_queued": "Na Fila",
"status_unknown": "Desconhecido",
"provider": "Provedor",
"streaming_playlist_warning": "Pode não reproduzir - playlist de streaming",
"remaining": "restantes",
"not_ready": "Download não pronto",
"not_ready_desc": "Por favor aguarde até que o download seja concluído.",
"filter_all": "Todos",
"filter_active": "Ativos",
"filter_done": "Concluídos",
"filter_paused": "Pausados",
"no_filter_results": "Nenhum download {{filter}}",
"try_different_filter": "Tente selecionar um filtro diferente",
"limitations_title": "Limitações de Download",
"limitations_msg": "• Arquivos menores que 1MB são tipicamente playlists de streaming M3U8 e não podem ser baixados para visualização offline. Eles funcionam apenas com streaming online e contêm links para segmentos de vídeo, não o conteúdo de vídeo real.",
"remove_title": "Remover Download",
"remove_confirm": "Remover \"{{title}}\"{{season_episode}}?",
"cancel": "Cancelar",
"remove": "Remover"
},
"addons": { "addons": {
"title": "Addons", "title": "Addons",
"reorder_mode": "Modo de Reordenação", "reorder_mode": "Modo de Reordenação",

View file

@ -30,6 +30,7 @@ import { LinearGradient } from 'expo-linear-gradient';
import FastImage from '@d11/react-native-fast-image'; import FastImage from '@d11/react-native-fast-image';
import { useDownloads } from '../contexts/DownloadsContext'; import { useDownloads } from '../contexts/DownloadsContext';
import { useSettings } from '../hooks/useSettings'; import { useSettings } from '../hooks/useSettings';
import { useTranslation } from 'react-i18next';
import { VideoPlayerService } from '../services/videoPlayerService'; import { VideoPlayerService } from '../services/videoPlayerService';
import type { DownloadItem } from '../contexts/DownloadsContext'; import type { DownloadItem } from '../contexts/DownloadsContext';
import { useToast } from '../contexts/ToastContext'; import { useToast } from '../contexts/ToastContext';
@ -65,6 +66,7 @@ const optimizePosterUrl = (poster: string | undefined | null): string => {
// Empty state component // Empty state component
const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamList> }> = ({ navigation }) => { const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamList> }> = ({ navigation }) => {
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { t } = useTranslation();
return ( return (
<View style={styles.emptyContainer}> <View style={styles.emptyContainer}>
@ -76,10 +78,10 @@ const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamL
/> />
</View> </View>
<Text style={[styles.emptyTitle, { color: currentTheme.colors.text }]}> <Text style={[styles.emptyTitle, { color: currentTheme.colors.text }]}>
No Downloads Yet {t('downloads.no_downloads')}
</Text> </Text>
<Text style={[styles.emptySubtitle, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.emptySubtitle, { color: currentTheme.colors.mediumEmphasis }]}>
Downloaded content will appear here for offline viewing {t('downloads.no_downloads_desc')}
</Text> </Text>
<TouchableOpacity <TouchableOpacity
style={[styles.exploreButton, { backgroundColor: currentTheme.colors.primary }]} style={[styles.exploreButton, { backgroundColor: currentTheme.colors.primary }]}
@ -88,7 +90,7 @@ const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamL
}} }}
> >
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.background }]}> <Text style={[styles.exploreButtonText, { color: currentTheme.colors.background }]}>
Explore Content {t('downloads.explore')}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@ -105,6 +107,7 @@ const DownloadItemComponent: React.FC<{
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { settings } = useSettings(); const { settings } = useSettings();
const { showSuccess, showInfo } = useToast(); const { showSuccess, showInfo } = useToast();
const { t } = useTranslation();
const [posterUrl, setPosterUrl] = useState<string | null>(item.posterUrl || null); const [posterUrl, setPosterUrl] = useState<string | null>(item.posterUrl || null);
const borderRadius = settings.posterBorderRadius ?? 12; const borderRadius = settings.posterBorderRadius ?? 12;
@ -121,15 +124,15 @@ const DownloadItemComponent: React.FC<{
if (item.status === 'completed' && item.fileUri) { if (item.status === 'completed' && item.fileUri) {
Clipboard.setString(item.fileUri); Clipboard.setString(item.fileUri);
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
showSuccess('Path Copied', 'Local file path copied to clipboard'); showSuccess(t('downloads.path_copied'), t('downloads.path_copied_desc'));
} else { } else {
Alert.alert('Copied', 'Local file path copied to clipboard'); Alert.alert(t('downloads.copied'), t('downloads.path_copied_desc'));
} }
} else if (item.status !== 'completed') { } else if (item.status !== 'completed') {
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
showInfo('Download Incomplete', 'Download is not complete yet'); showInfo(t('downloads.incomplete'), t('downloads.incomplete_desc'));
} else { } else {
Alert.alert('Not Available', 'The local file path is available only after the download is complete.'); Alert.alert(t('downloads.not_available'), t('downloads.not_available_desc'));
} }
} }
}, [item.status, item.fileUri, showSuccess, showInfo]); }, [item.status, item.fileUri, showSuccess, showInfo]);
@ -163,17 +166,17 @@ const DownloadItemComponent: React.FC<{
switch (item.status) { switch (item.status) {
case 'downloading': case 'downloading':
const eta = item.etaSeconds ? `${Math.ceil(item.etaSeconds / 60)}m` : undefined; const eta = item.etaSeconds ? `${Math.ceil(item.etaSeconds / 60)}m` : undefined;
return eta ? `Downloading • ${eta}` : 'Downloading'; return eta ? `${t('downloads.status_downloading')}${eta}` : t('downloads.status_downloading');
case 'completed': case 'completed':
return 'Completed'; return t('downloads.status_completed');
case 'paused': case 'paused':
return 'Paused'; return t('downloads.status_paused');
case 'error': case 'error':
return 'Error'; return t('downloads.status_error');
case 'queued': case 'queued':
return 'Queued'; return t('downloads.status_queued');
default: default:
return 'Unknown'; return t('downloads.status_unknown');
} }
}; };
@ -257,7 +260,7 @@ const DownloadItemComponent: React.FC<{
{/* Provider + quality row */} {/* Provider + quality row */}
<View style={styles.providerRow}> <View style={styles.providerRow}>
<Text style={[styles.providerText, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.providerText, { color: currentTheme.colors.mediumEmphasis }]}>
{item.providerName || 'Provider'} {item.providerName || t('downloads.provider')}
</Text> </Text>
</View> </View>
{/* Status row */} {/* Status row */}
@ -283,7 +286,7 @@ const DownloadItemComponent: React.FC<{
color={currentTheme.colors.warning || '#FF9500'} color={currentTheme.colors.warning || '#FF9500'}
/> />
<Text style={[styles.warningText, { color: currentTheme.colors.warning || '#FF9500' }]}> <Text style={[styles.warningText, { color: currentTheme.colors.warning || '#FF9500' }]}>
May not play - streaming playlist {t('downloads.streaming_playlist_warning')}
</Text> </Text>
</View> </View>
)} )}
@ -307,7 +310,7 @@ const DownloadItemComponent: React.FC<{
</Text> </Text>
{item.etaSeconds && item.status === 'downloading' && ( {item.etaSeconds && item.status === 'downloading' && (
<Text style={[styles.etaText, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.etaText, { color: currentTheme.colors.mediumEmphasis }]}>
{Math.ceil(item.etaSeconds / 60)}m remaining {Math.ceil(item.etaSeconds / 60)}m {t('downloads.remaining')}
</Text> </Text>
)} )}
</View> </View>
@ -350,6 +353,7 @@ const DownloadsScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { settings } = useSettings(); const { settings } = useSettings();
const { t } = useTranslation();
const { downloads, pauseDownload, resumeDownload, cancelDownload } = useDownloads(); const { downloads, pauseDownload, resumeDownload, cancelDownload } = useDownloads();
const { showSuccess, showInfo } = useToast(); const { showSuccess, showInfo } = useToast();
@ -409,7 +413,7 @@ const DownloadsScreen: React.FC = () => {
const handleDownloadPress = useCallback(async (item: DownloadItem) => { const handleDownloadPress = useCallback(async (item: DownloadItem) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
if (item.status !== 'completed') { if (item.status !== 'completed') {
Alert.alert('Download not ready', 'Please wait until the download completes.'); Alert.alert(t('downloads.not_ready'), t('downloads.not_ready_desc'));
return; return;
} }
const uri = (item as any).fileUri || (item as any).sourceUrl; const uri = (item as any).fileUri || (item as any).sourceUrl;
@ -636,7 +640,7 @@ const DownloadsScreen: React.FC = () => {
{/* ScreenHeader Component */} {/* ScreenHeader Component */}
<ScreenHeader <ScreenHeader
title="Downloads" title={t('downloads.title')}
rightActionComponent={ rightActionComponent={
<TouchableOpacity <TouchableOpacity
style={styles.helpButton} style={styles.helpButton}
@ -654,10 +658,10 @@ const DownloadsScreen: React.FC = () => {
> >
{downloads.length > 0 && ( {downloads.length > 0 && (
<View style={styles.filterContainer}> <View style={styles.filterContainer}>
{renderFilterButton('all', 'All', stats.total)} {renderFilterButton('all', t('downloads.filter_all'), stats.total)}
{renderFilterButton('downloading', 'Active', stats.downloading)} {renderFilterButton('downloading', t('downloads.filter_active'), stats.downloading)}
{renderFilterButton('completed', 'Done', stats.completed)} {renderFilterButton('completed', t('downloads.filter_done'), stats.completed)}
{renderFilterButton('paused', 'Paused', stats.paused)} {renderFilterButton('paused', t('downloads.filter_paused'), stats.paused)}
</View> </View>
)} )}
</ScreenHeader> </ScreenHeader>
@ -697,10 +701,10 @@ const DownloadsScreen: React.FC = () => {
color={currentTheme.colors.mediumEmphasis} color={currentTheme.colors.mediumEmphasis}
/> />
<Text style={[styles.emptyFilterTitle, { color: currentTheme.colors.text }]}> <Text style={[styles.emptyFilterTitle, { color: currentTheme.colors.text }]}>
No {selectedFilter} downloads {t('downloads.no_filter_results', { filter: selectedFilter })}
</Text> </Text>
<Text style={[styles.emptyFilterSubtitle, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.emptyFilterSubtitle, { color: currentTheme.colors.mediumEmphasis }]}>
Try selecting a different filter {t('downloads.try_different_filter')}
</Text> </Text>
</View> </View>
)} )}
@ -710,19 +714,22 @@ const DownloadsScreen: React.FC = () => {
{/* Help Alert */} {/* Help Alert */}
<CustomAlert <CustomAlert
visible={showHelpAlert} visible={showHelpAlert}
title="Download Limitations" title={t('downloads.limitations_title')}
message="• Files smaller than 1MB are typically M3U8 streaming playlists and cannot be downloaded for offline viewing. These only work with online streaming and contain links to video segments, not the actual video content." message={t('downloads.limitations_msg')}
onClose={() => setShowHelpAlert(false)} onClose={() => setShowHelpAlert(false)}
/> />
{/* Remove Download Confirmation */} {/* Remove Download Confirmation */}
<CustomAlert <CustomAlert
visible={showRemoveAlert} visible={showRemoveAlert}
title="Remove Download" title={t('downloads.remove_title')}
message={pendingRemoveItem ? `Remove \"${pendingRemoveItem.title}\"${pendingRemoveItem.type === 'series' && pendingRemoveItem.season && pendingRemoveItem.episode ? ` S${String(pendingRemoveItem.season).padStart(2, '0')}E${String(pendingRemoveItem.episode).padStart(2, '0')}` : ''}?` : 'Remove this download?'} message={pendingRemoveItem ? t('downloads.remove_confirm', {
title: pendingRemoveItem.title,
season_episode: pendingRemoveItem.type === 'series' && pendingRemoveItem.season && pendingRemoveItem.episode ? ` S${String(pendingRemoveItem.season).padStart(2, '0')}E${String(pendingRemoveItem.episode).padStart(2, '0')}` : ''
}) : t('downloads.remove_confirm', { title: 'this download', season_episode: '' })}
actions={[ actions={[
{ label: 'Cancel', onPress: () => setShowRemoveAlert(false) }, { label: t('downloads.cancel'), onPress: () => setShowRemoveAlert(false) },
{ label: 'Remove', onPress: () => { if (pendingRemoveItem) { cancelDownload(pendingRemoveItem.id); } setShowRemoveAlert(false); setPendingRemoveItem(null); }, style: {} }, { label: t('downloads.remove'), onPress: () => { if (pendingRemoveItem) { cancelDownload(pendingRemoveItem.id); } setShowRemoveAlert(false); setPendingRemoveItem(null); }, style: {} },
]} ]}
onClose={() => { setShowRemoveAlert(false); setPendingRemoveItem(null); }} onClose={() => { setShowRemoveAlert(false); setPendingRemoveItem(null); }}
/> />

View file

@ -1,4 +1,5 @@
import React, { useState, useEffect, useCallback, useRef, useMemo, useLayoutEffect } from 'react'; import React, { useState, useEffect, useCallback, useRef, useMemo, useLayoutEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { import {
View, View,
Text, Text,
@ -113,6 +114,7 @@ const SkeletonCatalog = React.memo(() => {
}); });
const HomeScreen = () => { const HomeScreen = () => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const isDarkMode = useColorScheme() === 'dark'; const isDarkMode = useColorScheme() === 'dark';
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
@ -288,7 +290,7 @@ const HomeScreen = () => {
displayName = uniqueWords.join(' '); displayName = uniqueWords.join(' ');
// Append content type if not present // Append content type if not present
const contentType = catalog.type === 'movie' ? 'Movies' : 'TV Shows'; const contentType = catalog.type === 'movie' ? t('home.movies') : t('home.tv_shows');
if (!displayName.toLowerCase().includes(contentType.toLowerCase())) { if (!displayName.toLowerCase().includes(contentType.toLowerCase())) {
displayName = `${displayName} ${contentType}`; displayName = `${displayName} ${contentType}`;
} }
@ -422,7 +424,7 @@ const HomeScreen = () => {
await mmkvStorage.removeItem('showLoginHintToastOnce'); await mmkvStorage.removeItem('showLoginHintToastOnce');
hideTimer = setTimeout(() => setHintVisible(false), 2000); hideTimer = setTimeout(() => setHintVisible(false), 2000);
// Also show a global toast for consistency across screens // Also show a global toast for consistency across screens
// showInfo('Sign In Available', 'You can sign in anytime from Settings → Account'); // showInfo(t('home.sign_in_available'), t('home.sign_in_desc'));
} }
} catch { } } catch { }
})(); })();
@ -813,7 +815,7 @@ const HomeScreen = () => {
> >
<MaterialIcons name="expand-more" size={20} color={currentTheme.colors.white} /> <MaterialIcons name="expand-more" size={20} color={currentTheme.colors.white} />
<Text style={[styles.loadMoreText, { color: currentTheme.colors.white }]}> <Text style={[styles.loadMoreText, { color: currentTheme.colors.white }]}>
Load More Catalogs {t('home.load_more_catalogs')}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
@ -835,14 +837,14 @@ const HomeScreen = () => {
<View style={[styles.emptyCatalog, { backgroundColor: currentTheme.colors.elevation1 }]}> <View style={[styles.emptyCatalog, { backgroundColor: currentTheme.colors.elevation1 }]}>
<MaterialIcons name="movie-filter" size={40} color={currentTheme.colors.textDark} /> <MaterialIcons name="movie-filter" size={40} color={currentTheme.colors.textDark} />
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}> <Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
No content available {t('home.no_content')}
</Text> </Text>
<TouchableOpacity <TouchableOpacity
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]} style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={() => navigation.navigate('Settings')} onPress={() => navigation.navigate('Settings')}
> >
<MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} /> <MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} />
<Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text> <Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>{t('home.add_catalogs')}</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
)} )}

View file

@ -38,6 +38,7 @@ import TraktIcon from '../../assets/rating-icons/trakt.svg';
import { traktService, TraktService, TraktImages } from '../services/traktService'; import { traktService, TraktService, TraktImages } from '../services/traktService';
import { TraktLoadingSpinner } from '../components/common/TraktLoadingSpinner'; import { TraktLoadingSpinner } from '../components/common/TraktLoadingSpinner';
import { useSettings } from '../hooks/useSettings'; import { useSettings } from '../hooks/useSettings';
import { useTranslation } from 'react-i18next';
import { useScrollToTop } from '../contexts/ScrollToTopContext'; import { useScrollToTop } from '../contexts/ScrollToTopContext';
interface LibraryItem extends StreamingContent { interface LibraryItem extends StreamingContent {
@ -211,6 +212,7 @@ const SkeletonLoader = () => {
}; };
const LibraryScreen = () => { const LibraryScreen = () => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const isDarkMode = useColorScheme() === 'dark'; const isDarkMode = useColorScheme() === 'dark';
const { width, height } = useWindowDimensions(); const { width, height } = useWindowDimensions();
@ -361,31 +363,31 @@ const LibraryScreen = () => {
const folders: TraktFolder[] = [ const folders: TraktFolder[] = [
{ {
id: 'watched', id: 'watched',
name: 'Watched', name: t('library.watched'),
icon: 'visibility', icon: 'visibility',
itemCount: (watchedMovies?.length || 0) + (watchedShows?.length || 0), itemCount: (watchedMovies?.length || 0) + (watchedShows?.length || 0),
}, },
{ {
id: 'continue-watching', id: 'continue-watching',
name: 'Continue', name: t('library.continue'),
icon: 'play-circle-outline', icon: 'play-circle-outline',
itemCount: continueWatching?.length || 0, itemCount: continueWatching?.length || 0,
}, },
{ {
id: 'watchlist', id: 'watchlist',
name: 'Watchlist', name: t('library.watchlist'),
icon: 'bookmark', icon: 'bookmark',
itemCount: (watchlistMovies?.length || 0) + (watchlistShows?.length || 0), itemCount: (watchlistMovies?.length || 0) + (watchlistShows?.length || 0),
}, },
{ {
id: 'collection', id: 'collection',
name: 'Collection', name: t('library.collection'),
icon: 'library-add', icon: 'library-add',
itemCount: (collectionMovies?.length || 0) + (collectionShows?.length || 0), itemCount: (collectionMovies?.length || 0) + (collectionShows?.length || 0),
}, },
{ {
id: 'ratings', id: 'ratings',
name: 'Rated', name: t('library.rated'),
icon: 'star', icon: 'star',
itemCount: ratedContent?.length || 0, itemCount: ratedContent?.length || 0,
} }
@ -457,7 +459,7 @@ const LibraryScreen = () => {
{folder.name} {folder.name}
</Text> </Text>
<Text style={styles.folderCount}> <Text style={styles.folderCount}>
{folder.itemCount} items {folder.itemCount} {t('library.items')}
</Text> </Text>
</View> </View>
</View> </View>
@ -487,14 +489,14 @@ const LibraryScreen = () => {
</Text> </Text>
{traktAuthenticated && traktFolders.length > 0 && ( {traktAuthenticated && traktFolders.length > 0 && (
<Text style={styles.folderCount}> <Text style={styles.folderCount}>
{traktFolders.length} items {traktFolders.length} {t('library.items')}
</Text> </Text>
)} )}
</View> </View>
</View> </View>
{settings.showPosterTitles && ( {settings.showPosterTitles && (
<Text style={[styles.cardTitle, { color: currentTheme.colors.mediumEmphasis }]}> <Text style={[styles.cardTitle, { color: currentTheme.colors.mediumEmphasis }]}>
Trakt collections {t('library.trakt_collections')}
</Text> </Text>
)} )}
</View> </View>
@ -720,9 +722,9 @@ const LibraryScreen = () => {
return ( return (
<View style={styles.emptyContainer}> <View style={styles.emptyContainer}>
<TraktIcon width={80} height={80} style={{ opacity: 0.7, marginBottom: 16 }} /> <TraktIcon width={80} height={80} style={{ opacity: 0.7, marginBottom: 16 }} />
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>No Trakt collections</Text> <Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>{t('library.no_trakt')}</Text>
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}> <Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
Your Trakt collections will appear here once you start using Trakt {t('library.no_trakt_desc')}
</Text> </Text>
<TouchableOpacity <TouchableOpacity
style={[styles.exploreButton, { style={[styles.exploreButton, {
@ -734,7 +736,7 @@ const LibraryScreen = () => {
}} }}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Load Collections</Text> <Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>{t('library.load_collections')}</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
); );
@ -758,13 +760,13 @@ const LibraryScreen = () => {
const folderItems = getTraktFolderItems(selectedTraktFolder); const folderItems = getTraktFolderItems(selectedTraktFolder);
if (folderItems.length === 0) { if (folderItems.length === 0) {
const folderName = traktFolders.find(f => f.id === selectedTraktFolder)?.name || 'Collection'; const folderName = traktFolders.find(f => f.id === selectedTraktFolder)?.name || t('library.collection');
return ( return (
<View style={styles.emptyContainer}> <View style={styles.emptyContainer}>
<TraktIcon width={80} height={80} style={{ opacity: 0.7, marginBottom: 16 }} /> <TraktIcon width={80} height={80} style={{ opacity: 0.7, marginBottom: 16 }} />
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>No content in {folderName}</Text> <Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>{t('library.empty_folder', { folder: folderName })}</Text>
<Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}> <Text style={[styles.emptySubtext, { color: currentTheme.colors.mediumGray }]}>
This collection is empty {t('library.empty_folder_desc')}
</Text> </Text>
<TouchableOpacity <TouchableOpacity
style={[styles.exploreButton, { style={[styles.exploreButton, {
@ -854,8 +856,8 @@ const LibraryScreen = () => {
} }
if (filteredItems.length === 0) { if (filteredItems.length === 0) {
const emptyTitle = filter === 'movies' ? 'No movies yet' : filter === 'series' ? 'No TV shows yet' : 'No content yet'; const emptyTitle = filter === 'movies' ? t('library.no_movies') : filter === 'series' ? t('library.no_series') : t('library.no_content');
const emptySubtitle = 'Add some content to your library to see it here'; const emptySubtitle = t('library.add_content_desc');
return ( return (
<View style={styles.emptyContainer}> <View style={styles.emptyContainer}>
<MaterialIcons <MaterialIcons
@ -877,7 +879,7 @@ const LibraryScreen = () => {
onPress={() => navigation.navigate('Search')} onPress={() => navigation.navigate('Search')}
activeOpacity={0.7} activeOpacity={0.7}
> >
<Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>Find something to watch</Text> <Text style={[styles.exploreButtonText, { color: currentTheme.colors.white }]}>{t('library.find_something')}</Text>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
); );
@ -908,9 +910,9 @@ const LibraryScreen = () => {
<ScreenHeader <ScreenHeader
title={showTraktContent title={showTraktContent
? (selectedTraktFolder ? (selectedTraktFolder
? traktFolders.find(f => f.id === selectedTraktFolder)?.name || 'Collection' ? traktFolders.find(f => f.id === selectedTraktFolder)?.name || t('library.collection')
: 'Trakt Collection') : t('library.trakt_collection'))
: 'Library' : t('library.title')
} }
showBackButton={showTraktContent} showBackButton={showTraktContent}
onBackPress={showTraktContent ? () => { onBackPress={showTraktContent ? () => {
@ -930,8 +932,8 @@ const LibraryScreen = () => {
{!showTraktContent && ( {!showTraktContent && (
<View style={styles.filtersContainer}> <View style={styles.filtersContainer}>
{renderFilter('trakt', 'Trakt', 'pan-tool')} {renderFilter('trakt', 'Trakt', 'pan-tool')}
{renderFilter('movies', 'Movies', 'movie')} {renderFilter('movies', t('search.movies'), 'movie')}
{renderFilter('series', 'TV Shows', 'live-tv')} {renderFilter('series', t('search.tv_shows'), 'live-tv')}
</View> </View>
)} )}
@ -951,11 +953,11 @@ const LibraryScreen = () => {
case 'library': { case 'library': {
try { try {
await catalogService.removeFromLibrary(selectedItem.type, selectedItem.id); await catalogService.removeFromLibrary(selectedItem.type, selectedItem.id);
showInfo('Removed from Library', 'Item removed from your library'); showInfo(t('library.removed_from_library'), t('library.item_removed'));
setLibraryItems(prev => prev.filter(item => !(item.id === selectedItem.id && item.type === selectedItem.type))); setLibraryItems(prev => prev.filter(item => !(item.id === selectedItem.id && item.type === selectedItem.type)));
setMenuVisible(false); setMenuVisible(false);
} catch (error) { } catch (error) {
showError('Failed to update Library', 'Unable to remove item from library'); showError(t('library.failed_update_library'), t('library.unable_remove'));
} }
break; break;
} }
@ -964,14 +966,14 @@ const LibraryScreen = () => {
const key = `watched:${selectedItem.type}:${selectedItem.id}`; const key = `watched:${selectedItem.type}:${selectedItem.id}`;
const newWatched = !selectedItem.watched; const newWatched = !selectedItem.watched;
await mmkvStorage.setItem(key, newWatched ? 'true' : 'false'); await mmkvStorage.setItem(key, newWatched ? 'true' : 'false');
showInfo(newWatched ? 'Marked as Watched' : 'Marked as Unwatched', newWatched ? 'Item marked as watched' : 'Item marked as unwatched'); showInfo(newWatched ? t('library.marked_watched') : t('library.marked_unwatched'), newWatched ? t('library.item_marked_watched') : t('library.item_marked_unwatched'));
setLibraryItems(prev => prev.map(item => setLibraryItems(prev => prev.map(item =>
item.id === selectedItem.id && item.type === selectedItem.type item.id === selectedItem.id && item.type === selectedItem.type
? { ...item, watched: newWatched } ? { ...item, watched: newWatched }
: item : item
)); ));
} catch (error) { } catch (error) {
showError('Failed to update watched status', 'Unable to update watched status'); showError(t('library.failed_update_watched'), t('library.unable_update_watched'));
} }
break; break;
} }

View file

@ -21,6 +21,7 @@ import {
} from 'react-native'; } from 'react-native';
import { useNavigation, useRoute, useFocusEffect } from '@react-navigation/native'; import { useNavigation, useRoute, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native';
import { useTranslation } from 'react-i18next';
import { MaterialIcons, Feather } from '@expo/vector-icons'; import { MaterialIcons, Feather } from '@expo/vector-icons';
import { catalogService, StreamingContent, GroupedSearchResults, AddonSearchResults } from '../services/catalogService'; import { catalogService, StreamingContent, GroupedSearchResults, AddonSearchResults } from '../services/catalogService';
import FastImage from '@d11/react-native-fast-image'; import FastImage from '@d11/react-native-fast-image';
@ -85,6 +86,7 @@ const SimpleSearchAnimation = SearchAnimation;
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
const SearchScreen = () => { const SearchScreen = () => {
const { t } = useTranslation();
const { settings } = useSettings(); const { settings } = useSettings();
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const isDarkMode = true; const isDarkMode = true;
@ -597,7 +599,7 @@ const SearchScreen = () => {
style={styles.recentSearchesContainer} style={styles.recentSearchesContainer}
> >
<Text style={[styles.carouselTitle, { color: currentTheme.colors.white }]}> <Text style={[styles.carouselTitle, { color: currentTheme.colors.white }]}>
Recent Searches {t('search.recent_searches')}
</Text> </Text>
{recentSearches.map((search, index) => ( {recentSearches.map((search, index) => (
<TouchableOpacity <TouchableOpacity
@ -705,7 +707,7 @@ const SearchScreen = () => {
{/* Section Header */} {/* Section Header */}
<View style={styles.discoverHeader}> <View style={styles.discoverHeader}>
<Text style={[styles.discoverTitle, { color: currentTheme.colors.white }]}> <Text style={[styles.discoverTitle, { color: currentTheme.colors.white }]}>
Discover {t('search.discover')}
</Text> </Text>
</View> </View>
@ -723,7 +725,7 @@ const SearchScreen = () => {
onPress={() => typeSheetRef.current?.present()} onPress={() => typeSheetRef.current?.present()}
> >
<Text style={[styles.discoverSelectorText, { color: currentTheme.colors.white }]} numberOfLines={1}> <Text style={[styles.discoverSelectorText, { color: currentTheme.colors.white }]} numberOfLines={1}>
{selectedDiscoverType === 'movie' ? 'Movies' : 'TV Shows'} {selectedDiscoverType === 'movie' ? t('search.movies') : t('search.tv_shows')}
</Text> </Text>
<MaterialIcons name="keyboard-arrow-down" size={20} color={currentTheme.colors.lightGray} /> <MaterialIcons name="keyboard-arrow-down" size={20} color={currentTheme.colors.lightGray} />
</TouchableOpacity> </TouchableOpacity>
@ -734,7 +736,7 @@ const SearchScreen = () => {
onPress={() => catalogSheetRef.current?.present()} onPress={() => catalogSheetRef.current?.present()}
> >
<Text style={[styles.discoverSelectorText, { color: currentTheme.colors.white }]} numberOfLines={1}> <Text style={[styles.discoverSelectorText, { color: currentTheme.colors.white }]} numberOfLines={1}>
{selectedCatalog ? selectedCatalog.catalogName : 'Select Catalog'} {selectedCatalog ? selectedCatalog.catalogName : t('search.select_catalog')}
</Text> </Text>
<MaterialIcons name="keyboard-arrow-down" size={20} color={currentTheme.colors.lightGray} /> <MaterialIcons name="keyboard-arrow-down" size={20} color={currentTheme.colors.lightGray} />
</TouchableOpacity> </TouchableOpacity>
@ -746,7 +748,7 @@ const SearchScreen = () => {
onPress={() => genreSheetRef.current?.present()} onPress={() => genreSheetRef.current?.present()}
> >
<Text style={[styles.discoverSelectorText, { color: currentTheme.colors.white }]} numberOfLines={1}> <Text style={[styles.discoverSelectorText, { color: currentTheme.colors.white }]} numberOfLines={1}>
{selectedDiscoverGenre || 'All Genres'} {selectedDiscoverGenre || t('search.all_genres')}
</Text> </Text>
<MaterialIcons name="keyboard-arrow-down" size={20} color={currentTheme.colors.lightGray} /> <MaterialIcons name="keyboard-arrow-down" size={20} color={currentTheme.colors.lightGray} />
</TouchableOpacity> </TouchableOpacity>
@ -757,7 +759,7 @@ const SearchScreen = () => {
{selectedCatalog && ( {selectedCatalog && (
<View style={styles.discoverFilterSummary}> <View style={styles.discoverFilterSummary}>
<Text style={[styles.discoverFilterSummaryText, { color: currentTheme.colors.lightGray }]}> <Text style={[styles.discoverFilterSummaryText, { color: currentTheme.colors.lightGray }]}>
{selectedCatalog.addonName} {selectedCatalog.type === 'movie' ? 'Movies' : 'TV Shows'} {selectedCatalog.addonName} {selectedCatalog.type === 'movie' ? t('search.movies') : t('search.tv_shows')}
{selectedDiscoverGenre ? `${selectedDiscoverGenre}` : ''} {selectedDiscoverGenre ? `${selectedDiscoverGenre}` : ''}
</Text> </Text>
</View> </View>
@ -768,7 +770,7 @@ const SearchScreen = () => {
<View style={styles.discoverLoadingContainer}> <View style={styles.discoverLoadingContainer}>
<ActivityIndicator size="large" color={currentTheme.colors.primary} /> <ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={[styles.discoverLoadingText, { color: currentTheme.colors.lightGray }]}> <Text style={[styles.discoverLoadingText, { color: currentTheme.colors.lightGray }]}>
Discovering content... {t('search.discovering')}
</Text> </Text>
</View> </View>
) : discoverResults.length > 0 ? ( ) : discoverResults.length > 0 ? (
@ -804,7 +806,7 @@ const SearchScreen = () => {
activeOpacity={0.7} activeOpacity={0.7}
> >
<Text style={[styles.showMoreButtonText, { color: currentTheme.colors.white }]}> <Text style={[styles.showMoreButtonText, { color: currentTheme.colors.white }]}>
Show More ({pendingDiscoverResults.length}) {t('search.show_more', { count: pendingDiscoverResults.length })}
</Text> </Text>
<MaterialIcons name="expand-more" size={20} color={currentTheme.colors.white} /> <MaterialIcons name="expand-more" size={20} color={currentTheme.colors.white} />
</TouchableOpacity> </TouchableOpacity>
@ -819,20 +821,20 @@ const SearchScreen = () => {
<View style={styles.discoverEmptyContainer}> <View style={styles.discoverEmptyContainer}>
<MaterialIcons name="movie-filter" size={48} color={currentTheme.colors.lightGray} /> <MaterialIcons name="movie-filter" size={48} color={currentTheme.colors.lightGray} />
<Text style={[styles.discoverEmptyText, { color: currentTheme.colors.lightGray }]}> <Text style={[styles.discoverEmptyText, { color: currentTheme.colors.lightGray }]}>
No content found {t('search.no_content_found')}
</Text> </Text>
<Text style={[styles.discoverEmptySubtext, { color: currentTheme.colors.mediumGray }]}> <Text style={[styles.discoverEmptySubtext, { color: currentTheme.colors.mediumGray }]}>
Try a different genre or catalog {t('search.try_different')}
</Text> </Text>
</View> </View>
) : !selectedCatalog && discoverInitialized ? ( ) : !selectedCatalog && discoverInitialized ? (
<View style={styles.discoverEmptyContainer}> <View style={styles.discoverEmptyContainer}>
<MaterialIcons name="touch-app" size={48} color={currentTheme.colors.lightGray} /> <MaterialIcons name="touch-app" size={48} color={currentTheme.colors.lightGray} />
<Text style={[styles.discoverEmptyText, { color: currentTheme.colors.lightGray }]}> <Text style={[styles.discoverEmptyText, { color: currentTheme.colors.lightGray }]}>
Select a catalog to discover {t('search.select_catalog_desc')}
</Text> </Text>
<Text style={[styles.discoverEmptySubtext, { color: currentTheme.colors.mediumGray }]}> <Text style={[styles.discoverEmptySubtext, { color: currentTheme.colors.mediumGray }]}>
Tap the catalog chip above to get started {t('search.tap_catalog_desc')}
</Text> </Text>
</View> </View>
) : null} ) : null}
@ -906,10 +908,10 @@ const SearchScreen = () => {
isGrid && styles.discoverGridItem isGrid && styles.discoverGridItem
]} ]}
onPress={() => { onPress={() => {
navigation.navigate('Metadata', { navigation.navigate('Metadata', {
id: item.id, id: item.id,
type: item.type, type: item.type,
addonId: item.addonId addonId: item.addonId
}); });
}} }}
onLongPress={() => { onLongPress={() => {
@ -1022,7 +1024,7 @@ const SearchScreen = () => {
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16 paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
} }
]}> ]}>
Movies ({movieResults.length}) {t('search.movies')} ({movieResults.length})
</Text> </Text>
<FlatList <FlatList
data={movieResults} data={movieResults}
@ -1056,7 +1058,7 @@ const SearchScreen = () => {
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16 paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
} }
]}> ]}>
TV Shows ({seriesResults.length}) {t('search.tv_shows')} ({seriesResults.length})
</Text> </Text>
<FlatList <FlatList
data={seriesResults} data={seriesResults}
@ -1176,7 +1178,7 @@ const SearchScreen = () => {
styles.searchInput, styles.searchInput,
{ color: currentTheme.colors.white } { color: currentTheme.colors.white }
]} ]}
placeholder="Search movies, shows..." placeholder={t('search.search_placeholder')}
placeholderTextColor={currentTheme.colors.lightGray} placeholderTextColor={currentTheme.colors.lightGray}
value={query} value={query}
onChangeText={setQuery} onChangeText={setQuery}
@ -1221,10 +1223,10 @@ const SearchScreen = () => {
color={currentTheme.colors.lightGray} color={currentTheme.colors.lightGray}
/> />
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}> <Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
Keep typing... {t('search.keep_typing')}
</Text> </Text>
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}> <Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
Type at least 2 characters to search {t('search.type_characters')}
</Text> </Text>
</View> </View>
) : searched && !hasResultsToShow ? ( ) : searched && !hasResultsToShow ? (
@ -1237,10 +1239,10 @@ const SearchScreen = () => {
color={currentTheme.colors.lightGray} color={currentTheme.colors.lightGray}
/> />
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}> <Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
No results found {t('search.no_results')}
</Text> </Text>
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}> <Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
Try different keywords or check your spelling {t('search.try_keywords')}
</Text> </Text>
</View> </View>
) : ( ) : (
@ -1343,7 +1345,7 @@ const SearchScreen = () => {
> >
<View style={[styles.bottomSheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}> <View style={[styles.bottomSheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}>
<Text style={[styles.bottomSheetTitle, { color: currentTheme.colors.white }]}> <Text style={[styles.bottomSheetTitle, { color: currentTheme.colors.white }]}>
Select Catalog {t('search.select_catalog')}
</Text> </Text>
<TouchableOpacity onPress={() => catalogSheetRef.current?.dismiss()}> <TouchableOpacity onPress={() => catalogSheetRef.current?.dismiss()}>
<MaterialIcons name="close" size={24} color={currentTheme.colors.lightGray} /> <MaterialIcons name="close" size={24} color={currentTheme.colors.lightGray} />
@ -1369,8 +1371,8 @@ const SearchScreen = () => {
{catalog.catalogName} {catalog.catalogName}
</Text> </Text>
<Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}> <Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}>
{catalog.addonName} {catalog.type === 'movie' ? 'Movies' : 'TV Shows'} {catalog.addonName} {catalog.type === 'movie' ? t('search.movies') : t('search.tv_shows')}
{catalog.genres.length > 0 ? `${catalog.genres.length} genres` : ''} {catalog.genres.length > 0 ? `${t('search.genres_count', { count: catalog.genres.length })}` : ''}
</Text> </Text>
</View> </View>
{selectedCatalog?.catalogId === catalog.catalogId && {selectedCatalog?.catalogId === catalog.catalogId &&
@ -1403,7 +1405,7 @@ const SearchScreen = () => {
> >
<View style={[styles.bottomSheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}> <View style={[styles.bottomSheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}>
<Text style={[styles.bottomSheetTitle, { color: currentTheme.colors.white }]}> <Text style={[styles.bottomSheetTitle, { color: currentTheme.colors.white }]}>
Select Genre {t('search.select_genre')}
</Text> </Text>
<TouchableOpacity onPress={() => genreSheetRef.current?.dismiss()}> <TouchableOpacity onPress={() => genreSheetRef.current?.dismiss()}>
<MaterialIcons name="close" size={24} color={currentTheme.colors.lightGray} /> <MaterialIcons name="close" size={24} color={currentTheme.colors.lightGray} />
@ -1423,10 +1425,10 @@ const SearchScreen = () => {
> >
<View style={styles.bottomSheetItemContent}> <View style={styles.bottomSheetItemContent}>
<Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}> <Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}>
All Genres {t('search.all_genres')}
</Text> </Text>
<Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}> <Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}>
Show all content {t('search.show_all_content')}
</Text> </Text>
</View> </View>
{!selectedDiscoverGenre && ( {!selectedDiscoverGenre && (
@ -1476,7 +1478,7 @@ const SearchScreen = () => {
> >
<View style={[styles.bottomSheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}> <View style={[styles.bottomSheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}>
<Text style={[styles.bottomSheetTitle, { color: currentTheme.colors.white }]}> <Text style={[styles.bottomSheetTitle, { color: currentTheme.colors.white }]}>
Select Type {t('search.select_type')}
</Text> </Text>
<TouchableOpacity onPress={() => typeSheetRef.current?.dismiss()}> <TouchableOpacity onPress={() => typeSheetRef.current?.dismiss()}>
<MaterialIcons name="close" size={24} color={currentTheme.colors.lightGray} /> <MaterialIcons name="close" size={24} color={currentTheme.colors.lightGray} />
@ -1496,10 +1498,10 @@ const SearchScreen = () => {
> >
<View style={styles.bottomSheetItemContent}> <View style={styles.bottomSheetItemContent}>
<Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}> <Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}>
Movies {t('search.movies')}
</Text> </Text>
<Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}> <Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}>
Browse movie catalogs {t('search.browse_movies')}
</Text> </Text>
</View> </View>
{selectedDiscoverType === 'movie' && ( {selectedDiscoverType === 'movie' && (
@ -1517,10 +1519,10 @@ const SearchScreen = () => {
> >
<View style={styles.bottomSheetItemContent}> <View style={styles.bottomSheetItemContent}>
<Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}> <Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}>
TV Shows {t('search.tv_shows')}
</Text> </Text>
<Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}> <Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}>
Browse TV series catalogs {t('search.browse_tv')}
</Text> </Text>
</View> </View>
{selectedDiscoverType === 'series' && ( {selectedDiscoverType === 'series' && (