mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
updated remaining main screens for localization
This commit is contained in:
parent
cdab715463
commit
6ef047db3c
15 changed files with 519 additions and 157 deletions
|
|
@ -12,6 +12,7 @@ import {
|
|||
Image,
|
||||
} from 'react-native';
|
||||
import { NavigationProp, useNavigation, useIsFocused } from '@react-navigation/native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
|
|
@ -144,6 +145,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
onRetry,
|
||||
scrollY: externalScrollY,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const isFocused = useIsFocused();
|
||||
const { currentTheme } = useTheme();
|
||||
|
|
@ -158,7 +160,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
const [inLibrary, setInLibrary] = useState(false);
|
||||
const [isInWatchlist, setIsInWatchlist] = useState(false);
|
||||
const [isWatched, setIsWatched] = useState(false);
|
||||
const [playButtonText, setPlayButtonText] = useState('Play');
|
||||
const [shouldResume, setShouldResume] = useState(false);
|
||||
const [type, setType] = useState<'movie' | 'series'>('movie');
|
||||
|
||||
// Create internal scrollY if not provided externally
|
||||
|
|
@ -530,7 +532,8 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
useEffect(() => {
|
||||
if (currentItem) {
|
||||
const buttonText = getProgressPlayButtonText();
|
||||
setPlayButtonText(buttonText);
|
||||
// Use internal state for resume logic instead of string comparison
|
||||
setShouldResume(buttonText === 'Resume');
|
||||
|
||||
// Update watched state based on progress
|
||||
if (watchProgress) {
|
||||
|
|
@ -987,10 +990,10 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
<View style={[styles.container, { height: HERO_HEIGHT, marginTop: -insets.top }]}>
|
||||
<View style={styles.noContentContainer}>
|
||||
<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 && (
|
||||
<TouchableOpacity style={styles.retryButton} onPress={onRetry} activeOpacity={0.7}>
|
||||
<Text style={styles.retryButtonText}>Retry</Text>
|
||||
<Text style={styles.retryButtonText}>{t('home.retry')}</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
|
@ -1242,7 +1245,7 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
<View style={styles.metadataBadge}>
|
||||
<MaterialIcons name="tv" size={16} color="#fff" />
|
||||
<Text style={styles.metadataText}>
|
||||
{currentItem.type === 'series' ? 'TV Show' : 'Movie'}
|
||||
{currentItem.type === 'series' ? t('home.tv_show') : t('home.movie')}
|
||||
</Text>
|
||||
{currentItem.genres && currentItem.genres.length > 0 && (
|
||||
<>
|
||||
|
|
@ -1262,11 +1265,11 @@ const AppleTVHero: React.FC<AppleTVHeroProps> = ({
|
|||
activeOpacity={0.85}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={playButtonText === 'Resume' ? "replay" : "play-arrow"}
|
||||
name={shouldResume ? "replay" : "play-arrow"}
|
||||
size={24}
|
||||
color="#000"
|
||||
/>
|
||||
<Text style={styles.playButtonText}>{playButtonText}</Text>
|
||||
<Text style={styles.playButtonText}>{shouldResume ? t('home.resume') : t('home.play')}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Save Button */}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Platform, Dimensions, FlatList } from 'react-native';
|
||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { CatalogContent, StreamingContent } from '../../services/catalogService';
|
||||
|
|
@ -73,6 +74,7 @@ const posterLayout = calculatePosterLayout(width);
|
|||
const POSTER_WIDTH = posterLayout.posterWidth;
|
||||
|
||||
const CatalogSection = ({ catalog }: CatalogSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
|
|
@ -154,7 +156,7 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
|
|||
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14,
|
||||
marginRight: isTV ? 6 : isLargeTablet ? 5 : 4,
|
||||
}
|
||||
]}>View All</Text>
|
||||
]}>{t('home.view_all')}</Text>
|
||||
<MaterialIcons
|
||||
name="chevron-right"
|
||||
size={isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import { DeviceEventEmitter } 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 ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, deferMs = 0 }: ContentItemProps) => {
|
||||
const { t } = useTranslation();
|
||||
// Track inLibrary status locally to force re-render
|
||||
const [inLibrary, setInLibrary] = useState(!!item.inLibrary);
|
||||
|
||||
|
|
@ -182,10 +184,10 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
|||
case 'library':
|
||||
if (inLibrary) {
|
||||
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 {
|
||||
catalogService.addToLibrary(item);
|
||||
showSuccess('Added to Library', 'Added to your local library');
|
||||
showSuccess(t('library.added_to_library'), t('library.item_added'));
|
||||
}
|
||||
break;
|
||||
case 'watched': {
|
||||
|
|
@ -194,7 +196,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
|||
try {
|
||||
await mmkvStorage.setItem(`watched:${item.type}:${item.id}`, targetWatched ? 'true' : 'false');
|
||||
} 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(() => {
|
||||
DeviceEventEmitter.emit('watchedStatusChanged');
|
||||
}, 100);
|
||||
|
|
@ -240,10 +242,10 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
|||
case 'trakt-watchlist': {
|
||||
if (isInWatchlist(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 {
|
||||
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);
|
||||
break;
|
||||
|
|
@ -251,10 +253,10 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
|
|||
case 'trakt-collection': {
|
||||
if (isInCollection(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 {
|
||||
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);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
Platform
|
||||
} from 'react-native';
|
||||
import { FlatList } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Animated, { FadeIn, Layout } from 'react-native-reanimated';
|
||||
import BottomSheet, { BottomSheetModal, BottomSheetView, BottomSheetBackdrop } from '@gorhom/bottom-sheet';
|
||||
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
|
||||
const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
const { settings } = useSettings();
|
||||
|
|
@ -1310,7 +1312,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
{/* Up Next Badge */}
|
||||
{item.type === 'series' && item.progress === 0 && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
|
@ -1441,7 +1443,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
<Text style={[
|
||||
styles.progressText,
|
||||
{ fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12 }
|
||||
]}>Up Next</Text>
|
||||
]}>{t('home.up_next')}</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
|
@ -1460,7 +1462,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 13
|
||||
}
|
||||
]}>
|
||||
Season {item.season}
|
||||
{t('home.season', { season: item.season })}
|
||||
</Text>
|
||||
{item.episodeTitle && (
|
||||
<Text
|
||||
|
|
@ -1487,7 +1489,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1519,7 +1521,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 11
|
||||
}
|
||||
]}>
|
||||
{Math.round(item.progress)}% watched
|
||||
{t('home.percent_watched', { percent: Math.round(item.progress) })}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -1558,7 +1560,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
color: currentTheme.colors.text,
|
||||
fontSize: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 26 : 24
|
||||
}
|
||||
]}>Continue Watching</Text>
|
||||
]}>{t('home.continue_watching')}</Text>
|
||||
<View style={[
|
||||
styles.titleUnderline,
|
||||
{
|
||||
|
|
@ -1631,12 +1633,12 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
</Text>
|
||||
{selectedItem.type === 'series' && selectedItem.season && selectedItem.episode ? (
|
||||
<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}`}
|
||||
</Text>
|
||||
) : (
|
||||
<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>
|
||||
)}
|
||||
{selectedItem.progress > 0 && (
|
||||
|
|
@ -1653,7 +1655,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
/>
|
||||
</View>
|
||||
<Text style={[styles.actionSheetProgressText, { color: currentTheme.colors.textMuted }]}>
|
||||
{Math.round(selectedItem.progress)}% watched
|
||||
{t('home.percent_watched', { percent: Math.round(selectedItem.progress) })}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -1668,7 +1670,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
activeOpacity={0.8}
|
||||
>
|
||||
<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
|
||||
|
|
@ -1677,7 +1679,7 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
|
|||
activeOpacity={0.8}
|
||||
>
|
||||
<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>
|
||||
</View>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
Dimensions,
|
||||
Platform
|
||||
} from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { useTraktContext } from '../../contexts/TraktContext';
|
||||
|
|
@ -39,6 +40,7 @@ interface DropUpMenuProps {
|
|||
}
|
||||
|
||||
export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: isSavedProp, isWatched: isWatchedProp }: DropUpMenuProps) => {
|
||||
const { t } = useTranslation();
|
||||
const translateY = useSharedValue(300);
|
||||
const opacity = useSharedValue(0);
|
||||
const isDarkMode = useColorScheme() === 'dark';
|
||||
|
|
@ -102,12 +104,12 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
|||
let menuOptions = [
|
||||
{
|
||||
icon: 'bookmark',
|
||||
label: isSaved ? 'Remove from Library' : 'Add to Library',
|
||||
label: isSaved ? t('library.remove_from_library') : t('library.add_to_library'),
|
||||
action: 'library'
|
||||
},
|
||||
{
|
||||
icon: 'check-circle',
|
||||
label: isWatched ? 'Mark as Unwatched' : 'Mark as Watched',
|
||||
label: isWatched ? t('library.mark_unwatched') : t('library.mark_watched'),
|
||||
action: 'watched'
|
||||
},
|
||||
/*
|
||||
|
|
@ -119,7 +121,7 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
|||
*/
|
||||
{
|
||||
icon: 'share',
|
||||
label: 'Share',
|
||||
label: t('library.share'),
|
||||
action: 'share'
|
||||
}
|
||||
];
|
||||
|
|
@ -129,12 +131,12 @@ export const DropUpMenu = ({ visible, onClose, item, onOptionSelect, isSaved: is
|
|||
menuOptions.push(
|
||||
{
|
||||
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'
|
||||
},
|
||||
{
|
||||
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'
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
Platform
|
||||
} from 'react-native';
|
||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RootStackParamList } from '../../navigation/AppNavigator';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
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 NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
|
|
@ -103,11 +105,11 @@ const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => {
|
|||
return (
|
||||
<View style={styles.noContentContainer}>
|
||||
<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}>
|
||||
{onRetry
|
||||
? 'There was a problem fetching featured content. Please check your connection and try again.'
|
||||
: 'Install addons with catalogs or change the content source in your settings.'}
|
||||
? t('home.load_error_desc')
|
||||
: t('home.no_featured_desc')}
|
||||
</Text>
|
||||
<View style={styles.noContentButtons}>
|
||||
{onRetry ? (
|
||||
|
|
@ -115,7 +117,7 @@ const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => {
|
|||
style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
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>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -123,13 +125,13 @@ const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => {
|
|||
style={[styles.noContentButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
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
|
||||
style={styles.noContentButton}
|
||||
onPress={() => navigation.navigate('HomeScreenSettings')}
|
||||
>
|
||||
<Text style={styles.noContentButtonText}>Settings</Text>
|
||||
<Text style={styles.noContentButtonText}>{t('home.settings')}</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -139,6 +141,7 @@ const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => {
|
|||
};
|
||||
|
||||
const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loading, onRetry }: FeaturedContentProps) => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
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} />
|
||||
<Text style={[styles.tabletPlayButtonText as TextStyle, { color: currentTheme.colors.black }]}>
|
||||
Play Now
|
||||
{t('home.play_now')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
|
@ -520,7 +523,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
>
|
||||
<MaterialIcons name={isSaved ? "bookmark" : "bookmark-outline"} size={20} 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>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
|
@ -531,7 +534,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
>
|
||||
<MaterialIcons name="info-outline" size={20} color={currentTheme.colors.white} />
|
||||
<Text style={[styles.tabletSecondaryButtonText as TextStyle, { color: currentTheme.colors.white }]}>
|
||||
More Info
|
||||
{t('home.more_info')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
|
|
@ -626,7 +629,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
>
|
||||
<MaterialIcons name={isSaved ? "bookmark" : "bookmark-outline"} size={24} 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>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
|
@ -644,7 +647,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
>
|
||||
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
|
||||
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
|
||||
Play
|
||||
{t('home.play')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
|
|
@ -655,7 +658,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
|||
>
|
||||
<MaterialIcons name="info-outline" size={24} color={currentTheme.colors.white} />
|
||||
<Text style={[styles.infoButtonText as TextStyle, { color: currentTheme.colors.white }]}>
|
||||
Info
|
||||
{t('home.info')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
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 { useTranslation } from 'react-i18next';
|
||||
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 { BlurView } from 'expo-blur';
|
||||
|
|
@ -38,6 +39,7 @@ interface HeroCarouselProps {
|
|||
const TOP_TABS_OFFSET = Platform.OS === 'ios' ? 44 : 48;
|
||||
|
||||
const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
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 { t } = useTranslation();
|
||||
const [bannerLoaded, setBannerLoaded] = useState(false);
|
||||
const [logoLoaded, setLogoLoaded] = useState(false);
|
||||
|
||||
|
|
@ -847,7 +850,7 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
|
|||
textShadowRadius: 2,
|
||||
}
|
||||
]}>
|
||||
{item.description || 'No description available'}
|
||||
{item.description || t('home.no_description')}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
|
@ -956,7 +959,7 @@ const CarouselCard: React.FC<CarouselCardProps> = memo(({ item, colors, logoFail
|
|||
textShadowRadius: 2,
|
||||
}
|
||||
]}>
|
||||
{item.description || 'No description available'}
|
||||
{item.description || t('home.no_description')}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
Dimensions
|
||||
} from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
|
|
@ -58,6 +59,7 @@ interface ThisWeekEpisode {
|
|||
}
|
||||
|
||||
export const ThisWeekSection = React.memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
const { calendarData, loading } = useCalendarData();
|
||||
|
|
@ -176,7 +178,7 @@ export const ThisWeekSection = React.memo(() => {
|
|||
processedItems.push({
|
||||
...firstEp,
|
||||
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,
|
||||
isGroup: true,
|
||||
episodeCount: group.length,
|
||||
|
|
@ -239,7 +241,7 @@ export const ThisWeekSection = React.memo(() => {
|
|||
const renderEpisodeItem = ({ item, index }: { item: ThisWeekEpisode, index: number }) => {
|
||||
// Handle episodes without release dates gracefully
|
||||
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;
|
||||
|
||||
// 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]}
|
||||
>
|
||||
<View style={styles.cardHeader}>
|
||||
<View style={[
|
||||
<View style={[
|
||||
styles.statusBadge,
|
||||
{ backgroundColor: isReleased ? currentTheme.colors.primary : 'rgba(0,0,0,0.6)' }
|
||||
]}>
|
||||
<Text style={styles.statusText}>
|
||||
{isReleased ? (item.isGroup ? 'Released' : 'New') : formattedDate}
|
||||
{isReleased ? (item.isGroup ? t('home.released') : t('home.new')) : formattedDate}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -357,7 +359,7 @@ export const ThisWeekSection = React.memo(() => {
|
|||
color: currentTheme.colors.text,
|
||||
fontSize: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 26 : 24
|
||||
}
|
||||
]}>This Week</Text>
|
||||
]}>{t('home.this_week')}</Text>
|
||||
<View style={[
|
||||
styles.titleUnderline,
|
||||
{
|
||||
|
|
@ -380,7 +382,7 @@ export const ThisWeekSection = React.memo(() => {
|
|||
color: currentTheme.colors.textMuted,
|
||||
fontSize: isTV ? 18 : isLargeTablet ? 16 : isTablet ? 15 : 14
|
||||
}
|
||||
]}>View All</Text>
|
||||
]}>{t('home.view_all')}</Text>
|
||||
<MaterialIcons
|
||||
name="chevron-right"
|
||||
size={isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20}
|
||||
|
|
|
|||
|
|
@ -5,20 +5,24 @@ import { mmkvStorage } from '../services/mmkvStorage';
|
|||
const languageDetector = {
|
||||
type: 'languageDetector',
|
||||
async: true,
|
||||
detect: async (callback: any) => {
|
||||
try {
|
||||
const savedLanguage = await mmkvStorage.getItem('user_language');
|
||||
if (savedLanguage) {
|
||||
callback(savedLanguage);
|
||||
return;
|
||||
detect: (callback?: (lng: string) => void): string | undefined => {
|
||||
const findLanguage = async () => {
|
||||
try {
|
||||
const savedLanguage = await mmkvStorage.getItem('user_language');
|
||||
if (savedLanguage) {
|
||||
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 languageCode = locales[0]?.languageCode ?? 'en';
|
||||
callback(languageCode);
|
||||
const locales = getLocales();
|
||||
const languageCode = locales[0]?.languageCode ?? 'en';
|
||||
if (callback) callback(languageCode);
|
||||
};
|
||||
findLanguage();
|
||||
return undefined;
|
||||
},
|
||||
init: () => { },
|
||||
cacheUserLanguage: (language: string) => {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,56 @@
|
|||
"ok": "OK",
|
||||
"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": {
|
||||
"home": "Home",
|
||||
"library": "Library",
|
||||
|
|
@ -18,6 +68,119 @@
|
|||
"downloads": "Downloads",
|
||||
"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": {
|
||||
"title": "Addons",
|
||||
"reorder_mode": "Reorder Mode",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,56 @@
|
|||
"ok": "OK",
|
||||
"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": {
|
||||
"home": "Início",
|
||||
"library": "Biblioteca",
|
||||
|
|
@ -18,6 +68,119 @@
|
|||
"downloads": "Downloads",
|
||||
"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": {
|
||||
"title": "Addons",
|
||||
"reorder_mode": "Modo de Reordenação",
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import { LinearGradient } from 'expo-linear-gradient';
|
|||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { useDownloads } from '../contexts/DownloadsContext';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { VideoPlayerService } from '../services/videoPlayerService';
|
||||
import type { DownloadItem } from '../contexts/DownloadsContext';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
|
|
@ -65,6 +66,7 @@ const optimizePosterUrl = (poster: string | undefined | null): string => {
|
|||
// Empty state component
|
||||
const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamList> }> = ({ navigation }) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View style={styles.emptyContainer}>
|
||||
|
|
@ -76,10 +78,10 @@ const EmptyDownloadsState: React.FC<{ navigation: NavigationProp<RootStackParamL
|
|||
/>
|
||||
</View>
|
||||
<Text style={[styles.emptyTitle, { color: currentTheme.colors.text }]}>
|
||||
No Downloads Yet
|
||||
{t('downloads.no_downloads')}
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtitle, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Downloaded content will appear here for offline viewing
|
||||
{t('downloads.no_downloads_desc')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
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 }]}>
|
||||
Explore Content
|
||||
{t('downloads.explore')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
@ -105,6 +107,7 @@ const DownloadItemComponent: React.FC<{
|
|||
const { currentTheme } = useTheme();
|
||||
const { settings } = useSettings();
|
||||
const { showSuccess, showInfo } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const [posterUrl, setPosterUrl] = useState<string | null>(item.posterUrl || null);
|
||||
const borderRadius = settings.posterBorderRadius ?? 12;
|
||||
|
||||
|
|
@ -121,15 +124,15 @@ const DownloadItemComponent: React.FC<{
|
|||
if (item.status === 'completed' && item.fileUri) {
|
||||
Clipboard.setString(item.fileUri);
|
||||
if (Platform.OS === 'android') {
|
||||
showSuccess('Path Copied', 'Local file path copied to clipboard');
|
||||
showSuccess(t('downloads.path_copied'), t('downloads.path_copied_desc'));
|
||||
} 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') {
|
||||
if (Platform.OS === 'android') {
|
||||
showInfo('Download Incomplete', 'Download is not complete yet');
|
||||
showInfo(t('downloads.incomplete'), t('downloads.incomplete_desc'));
|
||||
} 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]);
|
||||
|
|
@ -163,17 +166,17 @@ const DownloadItemComponent: React.FC<{
|
|||
switch (item.status) {
|
||||
case 'downloading':
|
||||
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':
|
||||
return 'Completed';
|
||||
return t('downloads.status_completed');
|
||||
case 'paused':
|
||||
return 'Paused';
|
||||
return t('downloads.status_paused');
|
||||
case 'error':
|
||||
return 'Error';
|
||||
return t('downloads.status_error');
|
||||
case 'queued':
|
||||
return 'Queued';
|
||||
return t('downloads.status_queued');
|
||||
default:
|
||||
return 'Unknown';
|
||||
return t('downloads.status_unknown');
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -257,7 +260,7 @@ const DownloadItemComponent: React.FC<{
|
|||
{/* Provider + quality row */}
|
||||
<View style={styles.providerRow}>
|
||||
<Text style={[styles.providerText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{item.providerName || 'Provider'}
|
||||
{item.providerName || t('downloads.provider')}
|
||||
</Text>
|
||||
</View>
|
||||
{/* Status row */}
|
||||
|
|
@ -283,7 +286,7 @@ const DownloadItemComponent: React.FC<{
|
|||
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>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -307,7 +310,7 @@ const DownloadItemComponent: React.FC<{
|
|||
</Text>
|
||||
{item.etaSeconds && item.status === 'downloading' && (
|
||||
<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>
|
||||
)}
|
||||
</View>
|
||||
|
|
@ -350,6 +353,7 @@ const DownloadsScreen: React.FC = () => {
|
|||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { currentTheme } = useTheme();
|
||||
const { settings } = useSettings();
|
||||
const { t } = useTranslation();
|
||||
const { downloads, pauseDownload, resumeDownload, cancelDownload } = useDownloads();
|
||||
const { showSuccess, showInfo } = useToast();
|
||||
|
||||
|
|
@ -409,7 +413,7 @@ const DownloadsScreen: React.FC = () => {
|
|||
const handleDownloadPress = useCallback(async (item: DownloadItem) => {
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
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;
|
||||
}
|
||||
const uri = (item as any).fileUri || (item as any).sourceUrl;
|
||||
|
|
@ -636,7 +640,7 @@ const DownloadsScreen: React.FC = () => {
|
|||
|
||||
{/* ScreenHeader Component */}
|
||||
<ScreenHeader
|
||||
title="Downloads"
|
||||
title={t('downloads.title')}
|
||||
rightActionComponent={
|
||||
<TouchableOpacity
|
||||
style={styles.helpButton}
|
||||
|
|
@ -654,10 +658,10 @@ const DownloadsScreen: React.FC = () => {
|
|||
>
|
||||
{downloads.length > 0 && (
|
||||
<View style={styles.filterContainer}>
|
||||
{renderFilterButton('all', 'All', stats.total)}
|
||||
{renderFilterButton('downloading', 'Active', stats.downloading)}
|
||||
{renderFilterButton('completed', 'Done', stats.completed)}
|
||||
{renderFilterButton('paused', 'Paused', stats.paused)}
|
||||
{renderFilterButton('all', t('downloads.filter_all'), stats.total)}
|
||||
{renderFilterButton('downloading', t('downloads.filter_active'), stats.downloading)}
|
||||
{renderFilterButton('completed', t('downloads.filter_done'), stats.completed)}
|
||||
{renderFilterButton('paused', t('downloads.filter_paused'), stats.paused)}
|
||||
</View>
|
||||
)}
|
||||
</ScreenHeader>
|
||||
|
|
@ -697,10 +701,10 @@ const DownloadsScreen: React.FC = () => {
|
|||
color={currentTheme.colors.mediumEmphasis}
|
||||
/>
|
||||
<Text style={[styles.emptyFilterTitle, { color: currentTheme.colors.text }]}>
|
||||
No {selectedFilter} downloads
|
||||
{t('downloads.no_filter_results', { filter: selectedFilter })}
|
||||
</Text>
|
||||
<Text style={[styles.emptyFilterSubtitle, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Try selecting a different filter
|
||||
{t('downloads.try_different_filter')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -710,19 +714,22 @@ const DownloadsScreen: React.FC = () => {
|
|||
{/* Help Alert */}
|
||||
<CustomAlert
|
||||
visible={showHelpAlert}
|
||||
title="Download Limitations"
|
||||
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."
|
||||
title={t('downloads.limitations_title')}
|
||||
message={t('downloads.limitations_msg')}
|
||||
onClose={() => setShowHelpAlert(false)}
|
||||
/>
|
||||
|
||||
{/* Remove Download Confirmation */}
|
||||
<CustomAlert
|
||||
visible={showRemoveAlert}
|
||||
title="Remove Download"
|
||||
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?'}
|
||||
title={t('downloads.remove_title')}
|
||||
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={[
|
||||
{ label: 'Cancel', onPress: () => setShowRemoveAlert(false) },
|
||||
{ label: 'Remove', onPress: () => { if (pendingRemoveItem) { cancelDownload(pendingRemoveItem.id); } setShowRemoveAlert(false); setPendingRemoveItem(null); }, style: {} },
|
||||
{ label: t('downloads.cancel'), onPress: () => setShowRemoveAlert(false) },
|
||||
{ label: t('downloads.remove'), onPress: () => { if (pendingRemoveItem) { cancelDownload(pendingRemoveItem.id); } setShowRemoveAlert(false); setPendingRemoveItem(null); }, style: {} },
|
||||
]}
|
||||
onClose={() => { setShowRemoveAlert(false); setPendingRemoveItem(null); }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { useState, useEffect, useCallback, useRef, useMemo, useLayoutEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
|
|
@ -113,6 +114,7 @@ const SkeletonCatalog = React.memo(() => {
|
|||
});
|
||||
|
||||
const HomeScreen = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const isDarkMode = useColorScheme() === 'dark';
|
||||
const { currentTheme } = useTheme();
|
||||
|
|
@ -288,7 +290,7 @@ const HomeScreen = () => {
|
|||
displayName = uniqueWords.join(' ');
|
||||
|
||||
// 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())) {
|
||||
displayName = `${displayName} ${contentType}`;
|
||||
}
|
||||
|
|
@ -422,7 +424,7 @@ const HomeScreen = () => {
|
|||
await mmkvStorage.removeItem('showLoginHintToastOnce');
|
||||
hideTimer = setTimeout(() => setHintVisible(false), 2000);
|
||||
// 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 { }
|
||||
})();
|
||||
|
|
@ -813,7 +815,7 @@ const HomeScreen = () => {
|
|||
>
|
||||
<MaterialIcons name="expand-more" size={20} color={currentTheme.colors.white} />
|
||||
<Text style={[styles.loadMoreText, { color: currentTheme.colors.white }]}>
|
||||
Load More Catalogs
|
||||
{t('home.load_more_catalogs')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
@ -835,14 +837,14 @@ const HomeScreen = () => {
|
|||
<View style={[styles.emptyCatalog, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||
<MaterialIcons name="movie-filter" size={40} color={currentTheme.colors.textDark} />
|
||||
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
|
||||
No content available
|
||||
{t('home.no_content')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={() => navigation.navigate('Settings')}
|
||||
>
|
||||
<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>
|
||||
</View>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import TraktIcon from '../../assets/rating-icons/trakt.svg';
|
|||
import { traktService, TraktService, TraktImages } from '../services/traktService';
|
||||
import { TraktLoadingSpinner } from '../components/common/TraktLoadingSpinner';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useScrollToTop } from '../contexts/ScrollToTopContext';
|
||||
|
||||
interface LibraryItem extends StreamingContent {
|
||||
|
|
@ -211,6 +212,7 @@ const SkeletonLoader = () => {
|
|||
};
|
||||
|
||||
const LibraryScreen = () => {
|
||||
const { t } = useTranslation();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const isDarkMode = useColorScheme() === 'dark';
|
||||
const { width, height } = useWindowDimensions();
|
||||
|
|
@ -361,31 +363,31 @@ const LibraryScreen = () => {
|
|||
const folders: TraktFolder[] = [
|
||||
{
|
||||
id: 'watched',
|
||||
name: 'Watched',
|
||||
name: t('library.watched'),
|
||||
icon: 'visibility',
|
||||
itemCount: (watchedMovies?.length || 0) + (watchedShows?.length || 0),
|
||||
},
|
||||
{
|
||||
id: 'continue-watching',
|
||||
name: 'Continue',
|
||||
name: t('library.continue'),
|
||||
icon: 'play-circle-outline',
|
||||
itemCount: continueWatching?.length || 0,
|
||||
},
|
||||
{
|
||||
id: 'watchlist',
|
||||
name: 'Watchlist',
|
||||
name: t('library.watchlist'),
|
||||
icon: 'bookmark',
|
||||
itemCount: (watchlistMovies?.length || 0) + (watchlistShows?.length || 0),
|
||||
},
|
||||
{
|
||||
id: 'collection',
|
||||
name: 'Collection',
|
||||
name: t('library.collection'),
|
||||
icon: 'library-add',
|
||||
itemCount: (collectionMovies?.length || 0) + (collectionShows?.length || 0),
|
||||
},
|
||||
{
|
||||
id: 'ratings',
|
||||
name: 'Rated',
|
||||
name: t('library.rated'),
|
||||
icon: 'star',
|
||||
itemCount: ratedContent?.length || 0,
|
||||
}
|
||||
|
|
@ -457,7 +459,7 @@ const LibraryScreen = () => {
|
|||
{folder.name}
|
||||
</Text>
|
||||
<Text style={styles.folderCount}>
|
||||
{folder.itemCount} items
|
||||
{folder.itemCount} {t('library.items')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -487,14 +489,14 @@ const LibraryScreen = () => {
|
|||
</Text>
|
||||
{traktAuthenticated && traktFolders.length > 0 && (
|
||||
<Text style={styles.folderCount}>
|
||||
{traktFolders.length} items
|
||||
{traktFolders.length} {t('library.items')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{settings.showPosterTitles && (
|
||||
<Text style={[styles.cardTitle, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Trakt collections
|
||||
{t('library.trakt_collections')}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
|
@ -720,9 +722,9 @@ const LibraryScreen = () => {
|
|||
return (
|
||||
<View style={styles.emptyContainer}>
|
||||
<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 }]}>
|
||||
Your Trakt collections will appear here once you start using Trakt
|
||||
{t('library.no_trakt_desc')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.exploreButton, {
|
||||
|
|
@ -734,7 +736,7 @@ const LibraryScreen = () => {
|
|||
}}
|
||||
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>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -758,13 +760,13 @@ const LibraryScreen = () => {
|
|||
const folderItems = getTraktFolderItems(selectedTraktFolder);
|
||||
|
||||
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 (
|
||||
<View style={styles.emptyContainer}>
|
||||
<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 }]}>
|
||||
This collection is empty
|
||||
{t('library.empty_folder_desc')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.exploreButton, {
|
||||
|
|
@ -854,8 +856,8 @@ const LibraryScreen = () => {
|
|||
}
|
||||
|
||||
if (filteredItems.length === 0) {
|
||||
const emptyTitle = filter === 'movies' ? 'No movies yet' : filter === 'series' ? 'No TV shows yet' : 'No content yet';
|
||||
const emptySubtitle = 'Add some content to your library to see it here';
|
||||
const emptyTitle = filter === 'movies' ? t('library.no_movies') : filter === 'series' ? t('library.no_series') : t('library.no_content');
|
||||
const emptySubtitle = t('library.add_content_desc');
|
||||
return (
|
||||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons
|
||||
|
|
@ -877,7 +879,7 @@ const LibraryScreen = () => {
|
|||
onPress={() => navigation.navigate('Search')}
|
||||
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>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -908,9 +910,9 @@ const LibraryScreen = () => {
|
|||
<ScreenHeader
|
||||
title={showTraktContent
|
||||
? (selectedTraktFolder
|
||||
? traktFolders.find(f => f.id === selectedTraktFolder)?.name || 'Collection'
|
||||
: 'Trakt Collection')
|
||||
: 'Library'
|
||||
? traktFolders.find(f => f.id === selectedTraktFolder)?.name || t('library.collection')
|
||||
: t('library.trakt_collection'))
|
||||
: t('library.title')
|
||||
}
|
||||
showBackButton={showTraktContent}
|
||||
onBackPress={showTraktContent ? () => {
|
||||
|
|
@ -930,8 +932,8 @@ const LibraryScreen = () => {
|
|||
{!showTraktContent && (
|
||||
<View style={styles.filtersContainer}>
|
||||
{renderFilter('trakt', 'Trakt', 'pan-tool')}
|
||||
{renderFilter('movies', 'Movies', 'movie')}
|
||||
{renderFilter('series', 'TV Shows', 'live-tv')}
|
||||
{renderFilter('movies', t('search.movies'), 'movie')}
|
||||
{renderFilter('series', t('search.tv_shows'), 'live-tv')}
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
|
@ -951,11 +953,11 @@ const LibraryScreen = () => {
|
|||
case 'library': {
|
||||
try {
|
||||
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)));
|
||||
setMenuVisible(false);
|
||||
} catch (error) {
|
||||
showError('Failed to update Library', 'Unable to remove item from library');
|
||||
showError(t('library.failed_update_library'), t('library.unable_remove'));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -964,14 +966,14 @@ const LibraryScreen = () => {
|
|||
const key = `watched:${selectedItem.type}:${selectedItem.id}`;
|
||||
const newWatched = !selectedItem.watched;
|
||||
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 =>
|
||||
item.id === selectedItem.id && item.type === selectedItem.type
|
||||
? { ...item, watched: newWatched }
|
||||
: item
|
||||
));
|
||||
} 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from 'react-native';
|
||||
import { useNavigation, useRoute, useFocusEffect } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MaterialIcons, Feather } from '@expo/vector-icons';
|
||||
import { catalogService, StreamingContent, GroupedSearchResults, AddonSearchResults } from '../services/catalogService';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
|
|
@ -85,6 +86,7 @@ const SimpleSearchAnimation = SearchAnimation;
|
|||
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
|
||||
|
||||
const SearchScreen = () => {
|
||||
const { t } = useTranslation();
|
||||
const { settings } = useSettings();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const isDarkMode = true;
|
||||
|
|
@ -597,7 +599,7 @@ const SearchScreen = () => {
|
|||
style={styles.recentSearchesContainer}
|
||||
>
|
||||
<Text style={[styles.carouselTitle, { color: currentTheme.colors.white }]}>
|
||||
Recent Searches
|
||||
{t('search.recent_searches')}
|
||||
</Text>
|
||||
{recentSearches.map((search, index) => (
|
||||
<TouchableOpacity
|
||||
|
|
@ -705,7 +707,7 @@ const SearchScreen = () => {
|
|||
{/* Section Header */}
|
||||
<View style={styles.discoverHeader}>
|
||||
<Text style={[styles.discoverTitle, { color: currentTheme.colors.white }]}>
|
||||
Discover
|
||||
{t('search.discover')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
|
@ -723,7 +725,7 @@ const SearchScreen = () => {
|
|||
onPress={() => typeSheetRef.current?.present()}
|
||||
>
|
||||
<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>
|
||||
<MaterialIcons name="keyboard-arrow-down" size={20} color={currentTheme.colors.lightGray} />
|
||||
</TouchableOpacity>
|
||||
|
|
@ -734,7 +736,7 @@ const SearchScreen = () => {
|
|||
onPress={() => catalogSheetRef.current?.present()}
|
||||
>
|
||||
<Text style={[styles.discoverSelectorText, { color: currentTheme.colors.white }]} numberOfLines={1}>
|
||||
{selectedCatalog ? selectedCatalog.catalogName : 'Select Catalog'}
|
||||
{selectedCatalog ? selectedCatalog.catalogName : t('search.select_catalog')}
|
||||
</Text>
|
||||
<MaterialIcons name="keyboard-arrow-down" size={20} color={currentTheme.colors.lightGray} />
|
||||
</TouchableOpacity>
|
||||
|
|
@ -746,7 +748,7 @@ const SearchScreen = () => {
|
|||
onPress={() => genreSheetRef.current?.present()}
|
||||
>
|
||||
<Text style={[styles.discoverSelectorText, { color: currentTheme.colors.white }]} numberOfLines={1}>
|
||||
{selectedDiscoverGenre || 'All Genres'}
|
||||
{selectedDiscoverGenre || t('search.all_genres')}
|
||||
</Text>
|
||||
<MaterialIcons name="keyboard-arrow-down" size={20} color={currentTheme.colors.lightGray} />
|
||||
</TouchableOpacity>
|
||||
|
|
@ -757,7 +759,7 @@ const SearchScreen = () => {
|
|||
{selectedCatalog && (
|
||||
<View style={styles.discoverFilterSummary}>
|
||||
<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}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -768,7 +770,7 @@ const SearchScreen = () => {
|
|||
<View style={styles.discoverLoadingContainer}>
|
||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.discoverLoadingText, { color: currentTheme.colors.lightGray }]}>
|
||||
Discovering content...
|
||||
{t('search.discovering')}
|
||||
</Text>
|
||||
</View>
|
||||
) : discoverResults.length > 0 ? (
|
||||
|
|
@ -804,7 +806,7 @@ const SearchScreen = () => {
|
|||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.showMoreButtonText, { color: currentTheme.colors.white }]}>
|
||||
Show More ({pendingDiscoverResults.length})
|
||||
{t('search.show_more', { count: pendingDiscoverResults.length })}
|
||||
</Text>
|
||||
<MaterialIcons name="expand-more" size={20} color={currentTheme.colors.white} />
|
||||
</TouchableOpacity>
|
||||
|
|
@ -819,20 +821,20 @@ const SearchScreen = () => {
|
|||
<View style={styles.discoverEmptyContainer}>
|
||||
<MaterialIcons name="movie-filter" size={48} color={currentTheme.colors.lightGray} />
|
||||
<Text style={[styles.discoverEmptyText, { color: currentTheme.colors.lightGray }]}>
|
||||
No content found
|
||||
{t('search.no_content_found')}
|
||||
</Text>
|
||||
<Text style={[styles.discoverEmptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||
Try a different genre or catalog
|
||||
{t('search.try_different')}
|
||||
</Text>
|
||||
</View>
|
||||
) : !selectedCatalog && discoverInitialized ? (
|
||||
<View style={styles.discoverEmptyContainer}>
|
||||
<MaterialIcons name="touch-app" size={48} color={currentTheme.colors.lightGray} />
|
||||
<Text style={[styles.discoverEmptyText, { color: currentTheme.colors.lightGray }]}>
|
||||
Select a catalog to discover
|
||||
{t('search.select_catalog_desc')}
|
||||
</Text>
|
||||
<Text style={[styles.discoverEmptySubtext, { color: currentTheme.colors.mediumGray }]}>
|
||||
Tap the catalog chip above to get started
|
||||
{t('search.tap_catalog_desc')}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
|
@ -906,10 +908,10 @@ const SearchScreen = () => {
|
|||
isGrid && styles.discoverGridItem
|
||||
]}
|
||||
onPress={() => {
|
||||
navigation.navigate('Metadata', {
|
||||
id: item.id,
|
||||
navigation.navigate('Metadata', {
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
addonId: item.addonId
|
||||
addonId: item.addonId
|
||||
});
|
||||
}}
|
||||
onLongPress={() => {
|
||||
|
|
@ -1022,7 +1024,7 @@ const SearchScreen = () => {
|
|||
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
|
||||
}
|
||||
]}>
|
||||
Movies ({movieResults.length})
|
||||
{t('search.movies')} ({movieResults.length})
|
||||
</Text>
|
||||
<FlatList
|
||||
data={movieResults}
|
||||
|
|
@ -1056,7 +1058,7 @@ const SearchScreen = () => {
|
|||
paddingHorizontal: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 16 : 16
|
||||
}
|
||||
]}>
|
||||
TV Shows ({seriesResults.length})
|
||||
{t('search.tv_shows')} ({seriesResults.length})
|
||||
</Text>
|
||||
<FlatList
|
||||
data={seriesResults}
|
||||
|
|
@ -1176,7 +1178,7 @@ const SearchScreen = () => {
|
|||
styles.searchInput,
|
||||
{ color: currentTheme.colors.white }
|
||||
]}
|
||||
placeholder="Search movies, shows..."
|
||||
placeholder={t('search.search_placeholder')}
|
||||
placeholderTextColor={currentTheme.colors.lightGray}
|
||||
value={query}
|
||||
onChangeText={setQuery}
|
||||
|
|
@ -1221,10 +1223,10 @@ const SearchScreen = () => {
|
|||
color={currentTheme.colors.lightGray}
|
||||
/>
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
||||
Keep typing...
|
||||
{t('search.keep_typing')}
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
|
||||
Type at least 2 characters to search
|
||||
{t('search.type_characters')}
|
||||
</Text>
|
||||
</View>
|
||||
) : searched && !hasResultsToShow ? (
|
||||
|
|
@ -1237,10 +1239,10 @@ const SearchScreen = () => {
|
|||
color={currentTheme.colors.lightGray}
|
||||
/>
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.white }]}>
|
||||
No results found
|
||||
{t('search.no_results')}
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.lightGray }]}>
|
||||
Try different keywords or check your spelling
|
||||
{t('search.try_keywords')}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
|
|
@ -1343,7 +1345,7 @@ const SearchScreen = () => {
|
|||
>
|
||||
<View style={[styles.bottomSheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}>
|
||||
<Text style={[styles.bottomSheetTitle, { color: currentTheme.colors.white }]}>
|
||||
Select Catalog
|
||||
{t('search.select_catalog')}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => catalogSheetRef.current?.dismiss()}>
|
||||
<MaterialIcons name="close" size={24} color={currentTheme.colors.lightGray} />
|
||||
|
|
@ -1369,8 +1371,8 @@ const SearchScreen = () => {
|
|||
{catalog.catalogName}
|
||||
</Text>
|
||||
<Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}>
|
||||
{catalog.addonName} • {catalog.type === 'movie' ? 'Movies' : 'TV Shows'}
|
||||
{catalog.genres.length > 0 ? ` • ${catalog.genres.length} genres` : ''}
|
||||
{catalog.addonName} • {catalog.type === 'movie' ? t('search.movies') : t('search.tv_shows')}
|
||||
{catalog.genres.length > 0 ? ` • ${t('search.genres_count', { count: catalog.genres.length })}` : ''}
|
||||
</Text>
|
||||
</View>
|
||||
{selectedCatalog?.catalogId === catalog.catalogId &&
|
||||
|
|
@ -1403,7 +1405,7 @@ const SearchScreen = () => {
|
|||
>
|
||||
<View style={[styles.bottomSheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}>
|
||||
<Text style={[styles.bottomSheetTitle, { color: currentTheme.colors.white }]}>
|
||||
Select Genre
|
||||
{t('search.select_genre')}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => genreSheetRef.current?.dismiss()}>
|
||||
<MaterialIcons name="close" size={24} color={currentTheme.colors.lightGray} />
|
||||
|
|
@ -1423,10 +1425,10 @@ const SearchScreen = () => {
|
|||
>
|
||||
<View style={styles.bottomSheetItemContent}>
|
||||
<Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}>
|
||||
All Genres
|
||||
{t('search.all_genres')}
|
||||
</Text>
|
||||
<Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}>
|
||||
Show all content
|
||||
{t('search.show_all_content')}
|
||||
</Text>
|
||||
</View>
|
||||
{!selectedDiscoverGenre && (
|
||||
|
|
@ -1476,7 +1478,7 @@ const SearchScreen = () => {
|
|||
>
|
||||
<View style={[styles.bottomSheetHeader, { backgroundColor: currentTheme.colors.darkGray || '#0A0C0C' }]}>
|
||||
<Text style={[styles.bottomSheetTitle, { color: currentTheme.colors.white }]}>
|
||||
Select Type
|
||||
{t('search.select_type')}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => typeSheetRef.current?.dismiss()}>
|
||||
<MaterialIcons name="close" size={24} color={currentTheme.colors.lightGray} />
|
||||
|
|
@ -1496,10 +1498,10 @@ const SearchScreen = () => {
|
|||
>
|
||||
<View style={styles.bottomSheetItemContent}>
|
||||
<Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}>
|
||||
Movies
|
||||
{t('search.movies')}
|
||||
</Text>
|
||||
<Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}>
|
||||
Browse movie catalogs
|
||||
{t('search.browse_movies')}
|
||||
</Text>
|
||||
</View>
|
||||
{selectedDiscoverType === 'movie' && (
|
||||
|
|
@ -1517,10 +1519,10 @@ const SearchScreen = () => {
|
|||
>
|
||||
<View style={styles.bottomSheetItemContent}>
|
||||
<Text style={[styles.bottomSheetItemTitle, { color: currentTheme.colors.white }]}>
|
||||
TV Shows
|
||||
{t('search.tv_shows')}
|
||||
</Text>
|
||||
<Text style={[styles.bottomSheetItemSubtitle, { color: currentTheme.colors.lightGray }]}>
|
||||
Browse TV series catalogs
|
||||
{t('search.browse_tv')}
|
||||
</Text>
|
||||
</View>
|
||||
{selectedDiscoverType === 'series' && (
|
||||
|
|
|
|||
Loading…
Reference in a new issue