mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
metascreen/streamscrean localization init
This commit is contained in:
parent
6ef047db3c
commit
5e3198c9c6
13 changed files with 391 additions and 93 deletions
|
|
@ -8,6 +8,7 @@ import {
|
|||
ActivityIndicator,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import Animated, {
|
||||
FadeIn,
|
||||
|
|
@ -35,6 +36,7 @@ export const CastSection: React.FC<CastSectionProps> = ({
|
|||
onSelectCastMember,
|
||||
isTmdbEnrichmentEnabled = true,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
// Enhanced responsive sizing for tablets and TV screens
|
||||
|
|
@ -137,7 +139,7 @@ export const CastSection: React.FC<CastSectionProps> = ({
|
|||
fontSize: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 18,
|
||||
marginBottom: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
|
||||
}
|
||||
]}>Cast</Text>
|
||||
]}>{t('metadata.cast')}</Text>
|
||||
</View>
|
||||
<FlatList
|
||||
horizontal
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
ActivityIndicator,
|
||||
Dimensions,
|
||||
} from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { useNavigation, StackActions } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
|
|
@ -39,6 +40,7 @@ export const CollectionSection: React.FC<CollectionSectionProps> = ({
|
|||
collectionMovies,
|
||||
loadingCollection
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { currentTheme } = useTheme();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
|
||||
|
|
@ -109,9 +111,9 @@ export const CollectionSection: React.FC<CollectionSectionProps> = ({
|
|||
}
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('Error navigating to collection item:', error);
|
||||
setAlertTitle('Error');
|
||||
setAlertMessage('Unable to load this content. Please try again later.');
|
||||
setAlertActions([{ label: 'OK', onPress: () => {} }]);
|
||||
setAlertTitle(t('common.error'));
|
||||
setAlertMessage(t('metadata.something_went_wrong'));
|
||||
setAlertActions([{ label: t('common.ok'), onPress: () => {} }]);
|
||||
setAlertVisible(true);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
Animated,
|
||||
Linking,
|
||||
} from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import TraktIcon from '../../../assets/rating-icons/trakt.svg';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
|
|
@ -186,6 +187,7 @@ const CompactCommentCard: React.FC<{
|
|||
isSpoilerRevealed: boolean;
|
||||
onSpoilerPress: () => void;
|
||||
}> = ({ comment, theme, onPress, isSpoilerRevealed, onSpoilerPress }) => {
|
||||
const { t } = useTranslation();
|
||||
const [isPressed, setIsPressed] = useState(false);
|
||||
const fadeInOpacity = useRef(new Animated.Value(0)).current;
|
||||
|
||||
|
|
@ -262,7 +264,7 @@ const CompactCommentCard: React.FC<{
|
|||
|
||||
// Handle missing user data gracefully
|
||||
const user = comment.user || {};
|
||||
const username = user.name || user.username || 'Anonymous';
|
||||
const username = user.name || user.username || t('common.anonymous_user');
|
||||
|
||||
// Handle spoiler content
|
||||
const hasSpoiler = comment.spoiler;
|
||||
|
|
@ -280,10 +282,10 @@ const CompactCommentCard: React.FC<{
|
|||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffMins < 1) return 'now';
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
if (diffMins < 1) return t('common.time.now');
|
||||
if (diffMins < 60) return t('common.time.minutes_ago', { count: diffMins });
|
||||
if (diffHours < 24) return t('common.time.hours_ago', { count: diffHours });
|
||||
if (diffDays < 7) return t('common.time.days_ago', { count: diffDays });
|
||||
|
||||
// For older dates, show month/day
|
||||
return commentDate.toLocaleDateString('en-US', {
|
||||
|
|
@ -725,6 +727,7 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
|
|||
episode,
|
||||
onCommentPress,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { currentTheme } = useTheme();
|
||||
const { settings } = useSettings();
|
||||
const [hasLoadedOnce, setHasLoadedOnce] = React.useState(false);
|
||||
|
|
@ -823,12 +826,12 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
|
|||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons name="chat-bubble-outline" size={48} color={currentTheme.colors.mediumEmphasis} />
|
||||
<Text style={[styles.emptyText, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
{error ? 'Comments unavailable' : 'No comments on Trakt yet'}
|
||||
{error ? t('comments.unavailable') : t('comments.no_comments')}
|
||||
</Text>
|
||||
<Text style={[styles.emptySubtext, { color: currentTheme.colors.disabled }]}>
|
||||
{error
|
||||
? 'This content may not be in Trakt\'s database yet'
|
||||
: 'Be the first to comment on Trakt.tv'
|
||||
? t('comments.not_in_database')
|
||||
: t('comments.check_trakt')
|
||||
}
|
||||
</Text>
|
||||
</View>
|
||||
|
|
@ -930,7 +933,7 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
|
|||
fontSize: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 20
|
||||
}
|
||||
]}>
|
||||
Trakt Comments
|
||||
{t('comments.title')}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
|
|
@ -945,7 +948,7 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
|
|||
onPress={refresh}
|
||||
>
|
||||
<Text style={[styles.retryButtonText, { color: currentTheme.colors.error }]}>
|
||||
Retry
|
||||
{t('common.retry')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
@ -993,7 +996,7 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
|
|||
) : (
|
||||
<>
|
||||
<Text style={[styles.loadMoreText, { color: currentTheme.colors.primary }]}>
|
||||
Load More
|
||||
{t('common.load_more')}
|
||||
</Text>
|
||||
<MaterialIcons name="chevron-right" size={20} color={currentTheme.colors.primary} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
Dimensions,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { useNavigation, StackActions } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
|
|
@ -39,6 +40,7 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
|||
recommendations,
|
||||
loadingRecommendations
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { currentTheme } = useTheme();
|
||||
const { settings } = useSettings();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
|
|
@ -112,9 +114,9 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
|||
}
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('Error navigating to recommendation:', error);
|
||||
setAlertTitle('Error');
|
||||
setAlertMessage('Unable to load this content. Please try again later.');
|
||||
setAlertActions([{ label: 'OK', onPress: () => { } }]);
|
||||
setAlertTitle(t('common.error'));
|
||||
setAlertMessage(t('metadata.something_went_wrong'));
|
||||
setAlertActions([{ label: t('common.ok'), onPress: () => { } }]);
|
||||
setAlertVisible(true);
|
||||
}
|
||||
};
|
||||
|
|
@ -149,7 +151,7 @@ export const MoreLikeThisSection: React.FC<MoreLikeThisSectionProps> = ({
|
|||
|
||||
return (
|
||||
<View style={[styles.container, { paddingLeft: 0 }]}>
|
||||
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis, fontSize: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20, paddingHorizontal: horizontalPadding }]}>More Like This</Text>
|
||||
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis, fontSize: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20, paddingHorizontal: horizontalPadding }]}>{t('metadata.more_like_this')}</Text>
|
||||
<FlatList
|
||||
data={recommendations}
|
||||
renderItem={renderItem}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
Platform,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { useTrailer } from '../../contexts/TrailerContext';
|
||||
import { logger } from '../../utils/logger';
|
||||
|
|
@ -19,24 +20,6 @@ import Video, { VideoRef, OnLoadData, OnProgressData } from 'react-native-video'
|
|||
const { width, height } = Dimensions.get('window');
|
||||
const isTablet = width >= 768;
|
||||
|
||||
// Helper function to format trailer type
|
||||
const formatTrailerType = (type: string): string => {
|
||||
switch (type) {
|
||||
case 'Trailer':
|
||||
return 'Official Trailer';
|
||||
case 'Teaser':
|
||||
return 'Teaser';
|
||||
case 'Clip':
|
||||
return 'Clip';
|
||||
case 'Featurette':
|
||||
return 'Featurette';
|
||||
case 'Behind the Scenes':
|
||||
return 'Behind the Scenes';
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
interface TrailerVideo {
|
||||
id: string;
|
||||
key: string;
|
||||
|
|
@ -61,8 +44,28 @@ const TrailerModal: React.FC<TrailerModalProps> = memo(({
|
|||
trailer,
|
||||
contentTitle
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { currentTheme } = useTheme();
|
||||
const { pauseTrailer, resumeTrailer } = useTrailer();
|
||||
|
||||
// Helper function to format trailer type with translations
|
||||
const formatTrailerType = useCallback((type: string): string => {
|
||||
switch (type) {
|
||||
case 'Trailer':
|
||||
return t('trailers.official_trailer');
|
||||
case 'Teaser':
|
||||
return t('trailers.teaser');
|
||||
case 'Clip':
|
||||
return t('trailers.clip');
|
||||
case 'Featurette':
|
||||
return t('trailers.featurette');
|
||||
case 'Behind the Scenes':
|
||||
return t('trailers.behind_the_scenes');
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}, [t]);
|
||||
|
||||
const videoRef = React.useRef<VideoRef>(null);
|
||||
const [trailerUrl, setTrailerUrl] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -126,9 +129,9 @@ const TrailerModal: React.FC<TrailerModalProps> = memo(({
|
|||
logger.error('TrailerModal', 'Error loading trailer:', err);
|
||||
|
||||
Alert.alert(
|
||||
'Trailer Unavailable',
|
||||
'This trailer could not be loaded at this time. Please try again later.',
|
||||
[{ text: 'OK', style: 'default' }]
|
||||
t('trailers.unavailable'),
|
||||
t('trailers.unavailable_desc'),
|
||||
[{ text: t('common.ok'), style: 'default' }]
|
||||
);
|
||||
}
|
||||
}, [trailer, contentTitle, pauseTrailer]);
|
||||
|
|
@ -232,7 +235,7 @@ const TrailerModal: React.FC<TrailerModalProps> = memo(({
|
|||
hitSlop={{ top: 10, left: 10, right: 10, bottom: 10 }}
|
||||
>
|
||||
<Text style={[styles.closeButtonText, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Close
|
||||
{t('common.close')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
@ -257,7 +260,7 @@ const TrailerModal: React.FC<TrailerModalProps> = memo(({
|
|||
style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={loadTrailer}
|
||||
>
|
||||
<Text style={styles.retryButtonText}>Try Again</Text>
|
||||
<Text style={styles.retryButtonText}>{t('common.try_again')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
ScrollView,
|
||||
Modal,
|
||||
} from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
|
|
@ -59,6 +60,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
contentId,
|
||||
contentTitle
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { currentTheme } = useTheme();
|
||||
const { settings } = useSettings();
|
||||
const { pauseTrailer } = useTrailer();
|
||||
|
|
@ -414,22 +416,22 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
};
|
||||
|
||||
// Format trailer type for display
|
||||
const formatTrailerType = (type: string): string => {
|
||||
const formatTrailerType = useCallback((type: string): string => {
|
||||
switch (type) {
|
||||
case 'Trailer':
|
||||
return 'Official Trailers';
|
||||
return t('trailers.official_trailers');
|
||||
case 'Teaser':
|
||||
return 'Teasers';
|
||||
return t('trailers.teasers');
|
||||
case 'Clip':
|
||||
return 'Clips & Scenes';
|
||||
return t('trailers.clips_scenes');
|
||||
case 'Featurette':
|
||||
return 'Featurettes';
|
||||
return t('trailers.featurettes');
|
||||
case 'Behind the Scenes':
|
||||
return 'Behind the Scenes';
|
||||
return t('trailers.behind_the_scenes');
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
};
|
||||
}, [t]);
|
||||
|
||||
// Get icon for trailer type
|
||||
const getTrailerTypeIcon = (type: string): string => {
|
||||
|
|
@ -483,12 +485,12 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
<View style={styles.header}>
|
||||
<MaterialIcons name="movie" size={20} color={currentTheme.colors.primary} />
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Trailers
|
||||
{t('trailers.title')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.noTrailersContainer}>
|
||||
<Text style={[styles.noTrailersText, { color: currentTheme.colors.textMuted }]}>
|
||||
No trailers available
|
||||
{t('trailers.no_trailers')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -512,7 +514,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
|
|||
fontSize: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 20
|
||||
}
|
||||
]}>
|
||||
Trailers & Videos
|
||||
{t('trailers.title')}
|
||||
</Text>
|
||||
|
||||
{/* Category Selector - Right Aligned */}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,22 @@
|
|||
"error": "Error",
|
||||
"success": "Success",
|
||||
"ok": "OK",
|
||||
"unknown": "Unknown"
|
||||
"unknown": "Unknown",
|
||||
"retry": "Retry",
|
||||
"try_again": "Try Again",
|
||||
"go_back": "Go Back",
|
||||
"close": "Close",
|
||||
"show_more": "Show More",
|
||||
"show_less": "Show Less",
|
||||
"load_more": "Load More",
|
||||
"unknown_date": "Unknown date",
|
||||
"anonymous_user": "Anonymous User",
|
||||
"time": {
|
||||
"now": "Just now",
|
||||
"minutes_ago": "{{count}}m ago",
|
||||
"hours_ago": "{{count}}h ago",
|
||||
"days_ago": "{{count}}d ago"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"categories": {
|
||||
|
|
@ -145,6 +160,128 @@
|
|||
"removed_from_collection": "Removed from Collection",
|
||||
"removed_from_collection_desc": "Removed from your Trakt collection"
|
||||
},
|
||||
"metadata": {
|
||||
"unable_to_load": "Unable to Load Content",
|
||||
"error_code": "Error Code: {{code}}",
|
||||
"content_not_found": "Content not found",
|
||||
"content_not_found_desc": "This content doesn't exist or may have been removed.",
|
||||
"server_error": "Server error",
|
||||
"server_error_desc": "The server is temporarily unavailable. Please try again later.",
|
||||
"bad_gateway": "Bad gateway",
|
||||
"bad_gateway_desc": "The server is experiencing issues. Please try again later.",
|
||||
"service_unavailable": "Service unavailable",
|
||||
"service_unavailable_desc": "The service is currently down for maintenance. Please try again later.",
|
||||
"too_many_requests": "Too many requests",
|
||||
"too_many_requests_desc": "You're making too many requests. Please wait a moment and try again.",
|
||||
"request_timeout": "Request timeout",
|
||||
"request_timeout_desc": "The request took too long. Please try again.",
|
||||
"network_error": "Network error",
|
||||
"network_error_desc": "Please check your internet connection and try again.",
|
||||
"auth_error": "Authentication error",
|
||||
"auth_error_desc": "Please check your account settings and try again.",
|
||||
"access_denied": "Access denied",
|
||||
"access_denied_desc": "You don't have permission to access this content.",
|
||||
"connection_error": "Connection error",
|
||||
"streams_unavailable": "Streams unavailable",
|
||||
"streams_unavailable_desc": "Streaming sources are currently unavailable. Please try again later.",
|
||||
"unknown_error": "Unknown error",
|
||||
"something_went_wrong": "Something went wrong. Please try again.",
|
||||
"cast": "Cast",
|
||||
"more_like_this": "More Like This",
|
||||
"collection": "Collection",
|
||||
"episodes": "Episodes",
|
||||
"seasons": "Seasons",
|
||||
"posters": "Posters",
|
||||
"banners": "Banners",
|
||||
"specials": "Specials",
|
||||
"season_number": "Season {{number}}",
|
||||
"episode_count": "{{count}} Episode",
|
||||
"episode_count_plural": "{{count}} Episodes",
|
||||
"no_episodes": "No episodes available",
|
||||
"no_episodes_for_season": "No episodes available for Season {{season}}",
|
||||
"episodes_not_released": "Episodes may not be released yet",
|
||||
"no_description": "No description available",
|
||||
"episode_label": "EPISODE {{number}}",
|
||||
"watch_again": "Watch Again",
|
||||
"completed": "Completed",
|
||||
"play_episode": "Play S{{season}}E{{episode}}",
|
||||
"play": "Play",
|
||||
"watched": "Watched",
|
||||
"watched_on_trakt": "Watched on Trakt",
|
||||
"synced_with_trakt": "Synced with Trakt",
|
||||
"saved": "Saved",
|
||||
"director": "Director",
|
||||
"directors": "Directors",
|
||||
"creator": "Creator",
|
||||
"creators": "Creators",
|
||||
"production": "Production",
|
||||
"network": "Network",
|
||||
"mark_watched": "Mark as Watched",
|
||||
"mark_unwatched": "Mark as Unwatched",
|
||||
"marking": "Marking...",
|
||||
"removing": "Removing...",
|
||||
"unmark_season": "Unmark Season {{season}}",
|
||||
"mark_season": "Mark Season {{season}}",
|
||||
"resume": "Resume"
|
||||
},
|
||||
"cast": {
|
||||
"biography": "Biography",
|
||||
"known_for": "Known For",
|
||||
"personal_info": "Personal Info",
|
||||
"born_in": "Born in {{place}}",
|
||||
"filmography": "Filmography",
|
||||
"also_known_as": "Also Known As",
|
||||
"no_info_available": "No additional information available"
|
||||
},
|
||||
"comments": {
|
||||
"title": "Trakt Comments",
|
||||
"spoiler_warning": "⚠️ This comment contains spoilers. Tap to reveal.",
|
||||
"spoiler": "Spoiler",
|
||||
"contains_spoilers": "Contains spoilers",
|
||||
"reveal": "Reveal",
|
||||
"vip": "VIP",
|
||||
"unavailable": "Comments unavailable",
|
||||
"no_comments": "No comments on Trakt yet",
|
||||
"not_in_database": "This content may not be in Trakt's database yet",
|
||||
"check_trakt": "Check Trakt"
|
||||
},
|
||||
"trailers": {
|
||||
"title": "Trailers",
|
||||
"official_trailers": "Official Trailers",
|
||||
"official_trailer": "Official Trailer",
|
||||
"teasers": "Teasers",
|
||||
"teaser": "Teaser",
|
||||
"clips_scenes": "Clips & Scenes",
|
||||
"clip": "Clip",
|
||||
"featurettes": "Featurettes",
|
||||
"featurette": "Featurette",
|
||||
"behind_the_scenes": "Behind the Scenes",
|
||||
"no_trailers": "No trailers available",
|
||||
"unavailable": "Trailer Unavailable",
|
||||
"unavailable_desc": "This trailer could not be loaded at this time. Please try again later.",
|
||||
"unable_to_play": "Unable to play trailer. Please try again.",
|
||||
"watch_on_youtube": "Watch on YouTube"
|
||||
},
|
||||
"catalog": {
|
||||
"no_content_found": "No content found",
|
||||
"no_content_filters": "No content found for the selected filters",
|
||||
"loading_content": "Loading content...",
|
||||
"back": "Back"
|
||||
},
|
||||
"streams": {
|
||||
"back_to_episodes": "Back to Episodes",
|
||||
"back_to_info": "Back to Info",
|
||||
"fetching_from": "Fetching from:",
|
||||
"no_sources_available": "No streaming sources available",
|
||||
"add_sources_desc": "Please add streaming sources in settings",
|
||||
"add_sources": "Add Sources",
|
||||
"finding_streams": "Finding available streams...",
|
||||
"finding_best_stream": "Finding best stream for autoplay...",
|
||||
"still_fetching": "Still fetching streams…",
|
||||
"no_streams_available": "No streams available",
|
||||
"starting_best_stream": "Starting best stream...",
|
||||
"loading_more_sources": "Loading more sources..."
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
"no_downloads": "No Downloads Yet",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,22 @@
|
|||
"error": "Erro",
|
||||
"success": "Sucesso",
|
||||
"ok": "OK",
|
||||
"unknown": "Desconhecido"
|
||||
"unknown": "Desconhecido",
|
||||
"retry": "Tentar Novamente",
|
||||
"try_again": "Tentar Novamente",
|
||||
"go_back": "Voltar",
|
||||
"close": "Fechar",
|
||||
"show_more": "Mostrar Mais",
|
||||
"show_less": "Mostrar Menos",
|
||||
"load_more": "Carregar Mais",
|
||||
"unknown_date": "Data desconhecida",
|
||||
"anonymous_user": "Usuário Anônimo",
|
||||
"time": {
|
||||
"now": "Agora",
|
||||
"minutes_ago": "{{count}}m atrás",
|
||||
"hours_ago": "{{count}}h atrás",
|
||||
"days_ago": "{{count}}d atrás"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"categories": {
|
||||
|
|
@ -145,6 +160,128 @@
|
|||
"removed_from_collection": "Removido da Coleção",
|
||||
"removed_from_collection_desc": "Removido da sua coleção Trakt"
|
||||
},
|
||||
"metadata": {
|
||||
"unable_to_load": "Não foi possível carregar o conteúdo",
|
||||
"error_code": "Código de Erro: {{code}}",
|
||||
"content_not_found": "Conteúdo não encontrado",
|
||||
"content_not_found_desc": "Este conteúdo não existe ou pode ter sido removido.",
|
||||
"server_error": "Erro do servidor",
|
||||
"server_error_desc": "O servidor está temporariamente indisponível. Por favor, tente novamente mais tarde.",
|
||||
"bad_gateway": "Gateway inválido",
|
||||
"bad_gateway_desc": "O servidor está com problemas. Por favor, tente novamente mais tarde.",
|
||||
"service_unavailable": "Serviço indisponível",
|
||||
"service_unavailable_desc": "O serviço está em manutenção. Por favor, tente novamente mais tarde.",
|
||||
"too_many_requests": "Muitas requisições",
|
||||
"too_many_requests_desc": "Você está fazendo muitas requisições. Por favor, aguarde um momento e tente novamente.",
|
||||
"request_timeout": "Tempo limite da requisição",
|
||||
"request_timeout_desc": "A requisição demorou muito. Por favor, tente novamente.",
|
||||
"network_error": "Erro de rede",
|
||||
"network_error_desc": "Por favor, verifique sua conexão com a internet e tente novamente.",
|
||||
"auth_error": "Erro de autenticação",
|
||||
"auth_error_desc": "Por favor, verifique as configurações da sua conta e tente novamente.",
|
||||
"access_denied": "Acesso negado",
|
||||
"access_denied_desc": "Você não tem permissão para acessar este conteúdo.",
|
||||
"connection_error": "Erro de conexão",
|
||||
"streams_unavailable": "Streams indisponíveis",
|
||||
"streams_unavailable_desc": "Fontes de streaming estão temporariamente indisponíveis. Por favor, tente novamente mais tarde.",
|
||||
"unknown_error": "Erro desconhecido",
|
||||
"something_went_wrong": "Algo deu errado. Por favor, tente novamente.",
|
||||
"cast": "Elenco",
|
||||
"more_like_this": "Mais Como Este",
|
||||
"collection": "Coleção",
|
||||
"episodes": "Episódios",
|
||||
"seasons": "Temporadas",
|
||||
"posters": "Pôsteres",
|
||||
"banners": "Banners",
|
||||
"specials": "Especiais",
|
||||
"season_number": "Temporada {{number}}",
|
||||
"episode_count": "{{count}} Episódio",
|
||||
"episode_count_plural": "{{count}} Episódios",
|
||||
"no_episodes": "Nenhum episódio disponível",
|
||||
"no_episodes_for_season": "Nenhum episódio disponível para a Temporada {{season}}",
|
||||
"episodes_not_released": "Os episódios podem ainda não ter sido lançados",
|
||||
"no_description": "Nenhuma descrição disponível",
|
||||
"episode_label": "EPISÓDIO {{number}}",
|
||||
"watch_again": "Assistir Novamente",
|
||||
"completed": "Concluído",
|
||||
"play_episode": "Reproduzir T{{season}}E{{episode}}",
|
||||
"play": "Reproduzir",
|
||||
"watched": "Assistido",
|
||||
"watched_on_trakt": "Assistido no Trakt",
|
||||
"synced_with_trakt": "Sincronizado com Trakt",
|
||||
"saved": "Salvo",
|
||||
"director": "Diretor",
|
||||
"directors": "Diretores",
|
||||
"creator": "Criador",
|
||||
"creators": "Criadores",
|
||||
"production": "Produção",
|
||||
"network": "Emissora",
|
||||
"mark_watched": "Marcar como Assistido",
|
||||
"mark_unwatched": "Marcar como Não Assistido",
|
||||
"marking": "Marcando...",
|
||||
"removing": "Removendo...",
|
||||
"unmark_season": "Desmarcar Temporada {{season}}",
|
||||
"mark_season": "Marcar Temporada {{season}}",
|
||||
"resume": "Continuar"
|
||||
},
|
||||
"cast": {
|
||||
"biography": "Biografia",
|
||||
"known_for": "Conhecido Por",
|
||||
"personal_info": "Informações Pessoais",
|
||||
"born_in": "Nascido em {{place}}",
|
||||
"filmography": "Filmografia",
|
||||
"also_known_as": "Também Conhecido Como",
|
||||
"no_info_available": "Nenhuma informação adicional disponível"
|
||||
},
|
||||
"comments": {
|
||||
"title": "Comentários do Trakt",
|
||||
"spoiler_warning": "⚠️ Este comentário contém spoilers. Toque para revelar.",
|
||||
"spoiler": "Spoiler",
|
||||
"contains_spoilers": "Contém spoilers",
|
||||
"reveal": "Revelar",
|
||||
"vip": "VIP",
|
||||
"unavailable": "Comentários indisponíveis",
|
||||
"no_comments": "Ainda não há comentários no Trakt",
|
||||
"not_in_database": "Este conteúdo pode ainda não estar no banco de dados do Trakt",
|
||||
"check_trakt": "Ver no Trakt"
|
||||
},
|
||||
"trailers": {
|
||||
"title": "Trailers",
|
||||
"official_trailers": "Trailers Oficiais",
|
||||
"official_trailer": "Trailer Oficial",
|
||||
"teasers": "Teasers",
|
||||
"teaser": "Teaser",
|
||||
"clips_scenes": "Clipes e Cenas",
|
||||
"clip": "Clipe",
|
||||
"featurettes": "Featurettes",
|
||||
"featurette": "Featurette",
|
||||
"behind_the_scenes": "Bastidores",
|
||||
"no_trailers": "Nenhum trailer disponível",
|
||||
"unavailable": "Trailer Indisponível",
|
||||
"unavailable_desc": "Este trailer não pôde ser carregado no momento. Por favor, tente novamente mais tarde.",
|
||||
"unable_to_play": "Não foi possível reproduzir o trailer. Por favor, tente novamente.",
|
||||
"watch_on_youtube": "Assistir no YouTube"
|
||||
},
|
||||
"catalog": {
|
||||
"no_content_found": "Nenhum conteúdo encontrado",
|
||||
"no_content_filters": "Nenhum conteúdo encontrado para os filtros selecionados",
|
||||
"loading_content": "Carregando conteúdo...",
|
||||
"back": "Voltar"
|
||||
},
|
||||
"streams": {
|
||||
"back_to_episodes": "Voltar aos Episódios",
|
||||
"back_to_info": "Voltar às Informações",
|
||||
"fetching_from": "Buscando de:",
|
||||
"no_sources_available": "Nenhuma fonte de streaming disponível",
|
||||
"add_sources_desc": "Por favor, adicione fontes de streaming nas configurações",
|
||||
"add_sources": "Adicionar Fontes",
|
||||
"finding_streams": "Procurando streams disponíveis...",
|
||||
"finding_best_stream": "Procurando melhor stream para reprodução automática...",
|
||||
"still_fetching": "Ainda buscando streams…",
|
||||
"no_streams_available": "Nenhum stream disponível",
|
||||
"starting_best_stream": "Iniciando melhor stream...",
|
||||
"loading_more_sources": "Carregando mais fontes..."
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Downloads",
|
||||
"no_downloads": "Nenhum Download Ainda",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
InteractionManager,
|
||||
ScrollView
|
||||
} from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { RouteProp } from '@react-navigation/native';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
|
|
@ -267,6 +268,7 @@ const createStyles = (colors: any) => StyleSheet.create({
|
|||
|
||||
const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
||||
const { addonId, type, id, name: originalName, genreFilter } = route.params;
|
||||
const { t } = useTranslation();
|
||||
const [items, setItems] = useState<Meta[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
|
@ -495,7 +497,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
return;
|
||||
} else {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setError("No content found for the selected filters");
|
||||
setError(t('catalog.no_content_filters'));
|
||||
setItems([]);
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
|
|
@ -665,7 +667,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
|
||||
if (!foundItems) {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
setError("No content found for the selected filters");
|
||||
setError(t('catalog.no_content_filters'));
|
||||
logger.log('[CatalogScreen] No items found after loading');
|
||||
});
|
||||
}
|
||||
|
|
@ -845,13 +847,13 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
<View style={styles.centered}>
|
||||
<MaterialIcons name="search-off" size={56} color={colors.mediumGray} />
|
||||
<Text style={styles.emptyText}>
|
||||
No content found
|
||||
{t('catalog.no_content_found')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.button}
|
||||
onPress={handleRefresh}
|
||||
>
|
||||
<Text style={styles.buttonText}>Try Again</Text>
|
||||
<Text style={styles.buttonText}>{t('common.try_again')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -866,7 +868,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
style={styles.button}
|
||||
onPress={() => loadItems(true)}
|
||||
>
|
||||
<Text style={styles.buttonText}>Retry</Text>
|
||||
<Text style={styles.buttonText}>{t('common.retry')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -874,7 +876,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
|
|||
const renderLoadingState = () => (
|
||||
<View style={styles.centered}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={styles.loadingText}>Loading content...</Text>
|
||||
<Text style={styles.loadingText}>{t('catalog.loading_content')}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
Platform,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useRoute, useNavigation, useFocusEffect } from '@react-navigation/native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
|
|
@ -88,6 +89,7 @@ const MetadataScreen: React.FC = () => {
|
|||
const route = useRoute<RouteProp<Record<string, RouteParams & { episodeId?: string; addonId?: string }>, string>>();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { id, type, episodeId, addonId } = route.params;
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Log route parameters for debugging
|
||||
React.useEffect(() => {
|
||||
|
|
@ -780,19 +782,19 @@ const MetadataScreen: React.FC = () => {
|
|||
console.log('✅ Found status code:', code);
|
||||
switch (code) {
|
||||
case 404:
|
||||
return { code: '404', message: 'Content not found', userMessage: 'This content doesn\'t exist or may have been removed.' };
|
||||
return { code: '404', message: t('metadata.content_not_found'), userMessage: t('metadata.content_not_found_desc') };
|
||||
case 500:
|
||||
return { code: '500', message: 'Server error', userMessage: 'The server is temporarily unavailable. Please try again later.' };
|
||||
return { code: '500', message: t('metadata.server_error'), userMessage: t('metadata.server_error_desc') };
|
||||
case 502:
|
||||
return { code: '502', message: 'Bad gateway', userMessage: 'The server is experiencing issues. Please try again later.' };
|
||||
return { code: '502', message: t('metadata.bad_gateway'), userMessage: t('metadata.bad_gateway_desc') };
|
||||
case 503:
|
||||
return { code: '503', message: 'Service unavailable', userMessage: 'The service is currently down for maintenance. Please try again later.' };
|
||||
return { code: '503', message: t('metadata.service_unavailable'), userMessage: t('metadata.service_unavailable_desc') };
|
||||
case 429:
|
||||
return { code: '429', message: 'Too many requests', userMessage: 'You\'re making too many requests. Please wait a moment and try again.' };
|
||||
return { code: '429', message: t('metadata.too_many_requests'), userMessage: t('metadata.too_many_requests_desc') };
|
||||
case 408:
|
||||
return { code: '408', message: 'Request timeout', userMessage: 'The request took too long. Please try again.' };
|
||||
return { code: '408', message: t('metadata.request_timeout'), userMessage: t('metadata.request_timeout_desc') };
|
||||
default:
|
||||
return { code: code.toString(), message: `Error ${code}`, userMessage: 'Something went wrong. Please try again.' };
|
||||
return { code: code.toString(), message: `Error ${code}`, userMessage: t('metadata.something_went_wrong') };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -801,7 +803,7 @@ const MetadataScreen: React.FC = () => {
|
|||
error.includes('ERR_BAD_RESPONSE') ||
|
||||
error.includes('Request failed') ||
|
||||
error.includes('ERR_NETWORK')) {
|
||||
return { code: 'NETWORK', message: 'Network error', userMessage: 'Please check your internet connection and try again.' };
|
||||
return { code: 'NETWORK', message: t('metadata.network_error'), userMessage: t('metadata.network_error_desc') };
|
||||
}
|
||||
|
||||
// Check for timeout errors
|
||||
|
|
@ -809,36 +811,36 @@ const MetadataScreen: React.FC = () => {
|
|||
error.includes('timed out') ||
|
||||
error.includes('ECONNABORTED') ||
|
||||
error.includes('ETIMEDOUT')) {
|
||||
return { code: 'TIMEOUT', message: 'Request timeout', userMessage: 'The request took too long. Please try again.' };
|
||||
return { code: 'TIMEOUT', message: t('metadata.request_timeout'), userMessage: t('metadata.request_timeout_desc') };
|
||||
}
|
||||
|
||||
// Check for authentication errors
|
||||
if (error.includes('401') || error.includes('Unauthorized') || error.includes('authentication')) {
|
||||
return { code: '401', message: 'Authentication error', userMessage: 'Please check your account settings and try again.' };
|
||||
return { code: '401', message: t('metadata.auth_error'), userMessage: t('metadata.auth_error_desc') };
|
||||
}
|
||||
|
||||
// Check for permission errors
|
||||
if (error.includes('403') || error.includes('Forbidden') || error.includes('permission')) {
|
||||
return { code: '403', message: 'Access denied', userMessage: 'You don\'t have permission to access this content.' };
|
||||
return { code: '403', message: t('metadata.access_denied'), userMessage: t('metadata.access_denied_desc') };
|
||||
}
|
||||
|
||||
// Check for "not found" errors - but only if no status code was found
|
||||
if (!statusCodeMatch && (error.includes('Content not found') || error.includes('not found'))) {
|
||||
return { code: '404', message: 'Content not found', userMessage: 'This content doesn\'t exist or may have been removed.' };
|
||||
return { code: '404', message: t('metadata.content_not_found'), userMessage: t('metadata.content_not_found_desc') };
|
||||
}
|
||||
|
||||
// Check for retry/attempt errors
|
||||
if (error.includes('attempts') || error.includes('Please check your connection')) {
|
||||
return { code: 'CONNECTION', message: 'Connection error', userMessage: 'Please check your internet connection and try again.' };
|
||||
return { code: 'CONNECTION', message: t('metadata.connection_error'), userMessage: t('metadata.network_error_desc') };
|
||||
}
|
||||
|
||||
// Check for streams-related errors
|
||||
if (error.includes('streams') || error.includes('Failed to load streams')) {
|
||||
return { code: 'STREAMS', message: 'Streams unavailable', userMessage: 'Streaming sources are currently unavailable. Please try again later.' };
|
||||
return { code: 'STREAMS', message: t('metadata.streams_unavailable'), userMessage: t('metadata.streams_unavailable_desc') };
|
||||
}
|
||||
|
||||
// Default case
|
||||
return { code: 'UNKNOWN', message: 'Unknown error', userMessage: 'An unexpected error occurred. Please try again.' };
|
||||
return { code: 'UNKNOWN', message: t('metadata.unknown_error'), userMessage: t('metadata.something_went_wrong') };
|
||||
};
|
||||
|
||||
const errorInfo = parseError(metadataError);
|
||||
|
|
@ -852,10 +854,10 @@ const MetadataScreen: React.FC = () => {
|
|||
<View style={styles.errorContainer}>
|
||||
<MaterialIcons name="error-outline" size={64} color={currentTheme.colors.error || '#FF6B6B'} />
|
||||
<Text style={[styles.errorTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Unable to Load Content
|
||||
{t('metadata.unable_to_load')}
|
||||
</Text>
|
||||
<Text style={[styles.errorCode, { color: currentTheme.colors.textMuted }]}>
|
||||
Error Code: {errorInfo.code}
|
||||
{t('metadata.error_code', { code: errorInfo.code })}
|
||||
</Text>
|
||||
<Text style={[styles.errorMessage, { color: currentTheme.colors.highEmphasis }]}>
|
||||
{errorInfo.userMessage}
|
||||
|
|
@ -870,13 +872,13 @@ const MetadataScreen: React.FC = () => {
|
|||
onPress={loadMetadata}
|
||||
>
|
||||
<MaterialIcons name="refresh" size={20} color={currentTheme.colors.white} style={{ marginRight: 8 }} />
|
||||
<Text style={styles.retryButtonText}>Try Again</Text>
|
||||
<Text style={styles.retryButtonText}>{t('common.try_again')}</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.backButton, { borderColor: currentTheme.colors.primary }]}
|
||||
onPress={handleBack}
|
||||
>
|
||||
<Text style={[styles.backButtonText, { color: currentTheme.colors.primary }]}>Go Back</Text>
|
||||
<Text style={[styles.backButtonText, { color: currentTheme.colors.primary }]}>{t('common.go_back')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { memo } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, StatusBar, Platform } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { PaperProvider } from 'react-native-paper';
|
||||
|
|
@ -88,6 +89,7 @@ export const StreamsScreen = () => {
|
|||
gradientColors,
|
||||
} = useStreamsScreen();
|
||||
|
||||
const { t } = useTranslation();
|
||||
const styles = React.useMemo(() => createStyles(colors), [colors]);
|
||||
|
||||
return (
|
||||
|
|
@ -106,8 +108,8 @@ export const StreamsScreen = () => {
|
|||
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
|
||||
<Text style={styles.backButtonText}>
|
||||
{metadata?.videos && metadata.videos.length > 1 && selectedEpisode
|
||||
? 'Back to Episodes'
|
||||
: 'Back to Info'}
|
||||
? t('streams.back_to_episodes')
|
||||
: t('streams.back_to_info')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { memo } from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Platform } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { BlurView as ExpoBlurView } from 'expo-blur';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
|
|
@ -129,6 +130,7 @@ const MobileStreamsLayout = memo(
|
|||
id,
|
||||
imdbId,
|
||||
}: MobileStreamsLayoutProps) => {
|
||||
const { t } = useTranslation();
|
||||
const styles = React.useMemo(() => createStyles(colors), [colors]);
|
||||
const isEpisode = metadata?.videos && metadata.videos.length > 1 && selectedEpisode;
|
||||
|
||||
|
|
@ -227,7 +229,7 @@ const MobileStreamsLayout = memo(
|
|||
{/* Active Scrapers Status */}
|
||||
{activeFetchingScrapers.length > 0 && (
|
||||
<View style={styles.activeScrapersContainer}>
|
||||
<Text style={styles.activeScrapersTitle}>Fetching from:</Text>
|
||||
<Text style={styles.activeScrapersTitle}>{t('streams.fetching_from')}</Text>
|
||||
<View style={styles.activeScrapersRow}>
|
||||
{activeFetchingScrapers.map((scraperName, index) => (
|
||||
<PulsingChip key={scraperName} text={scraperName} delay={index * 200} />
|
||||
|
|
@ -240,13 +242,13 @@ const MobileStreamsLayout = memo(
|
|||
{showNoSourcesError ? (
|
||||
<View style={styles.noStreams}>
|
||||
<MaterialIcons name="error-outline" size={48} color={colors.textMuted} />
|
||||
<Text style={styles.noStreamsText}>No streaming sources available</Text>
|
||||
<Text style={styles.noStreamsSubText}>Please add streaming sources in settings</Text>
|
||||
<Text style={styles.noStreamsText}>{t('streams.no_sources_available')}</Text>
|
||||
<Text style={styles.noStreamsSubText}>{t('streams.add_sources_desc')}</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.addSourcesButton}
|
||||
onPress={() => navigation.navigate('Addons' as never)}
|
||||
>
|
||||
<Text style={styles.addSourcesButtonText}>Add Sources</Text>
|
||||
<Text style={styles.addSourcesButtonText}>{t('streams.add_sources')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : streamsEmpty ? (
|
||||
|
|
@ -254,18 +256,18 @@ const MobileStreamsLayout = memo(
|
|||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
<Text style={styles.loadingText}>
|
||||
{isAutoplayWaiting ? 'Finding best stream for autoplay...' : 'Finding available streams...'}
|
||||
{isAutoplayWaiting ? t('streams.finding_best_stream') : t('streams.finding_streams')}
|
||||
</Text>
|
||||
</View>
|
||||
) : showStillFetching ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<MaterialIcons name="hourglass-bottom" size={32} color={colors.primary} />
|
||||
<Text style={styles.loadingText}>Still fetching streams…</Text>
|
||||
<Text style={styles.loadingText}>{t('streams.still_fetching')}</Text>
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.noStreams}>
|
||||
<MaterialIcons name="error-outline" size={48} color={colors.textMuted} />
|
||||
<Text style={styles.noStreamsText}>No streams available</Text>
|
||||
<Text style={styles.noStreamsText}>{t('streams.no_streams_available')}</Text>
|
||||
</View>
|
||||
)
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
ActivityIndicator,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LegendList } from '@legendapp/list';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
|
|
@ -59,6 +60,7 @@ const StreamsList = memo(
|
|||
id,
|
||||
imdbId,
|
||||
}: StreamsListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const insets = useSafeAreaInsets();
|
||||
const styles = React.useMemo(() => createStyles(colors), [colors]);
|
||||
|
||||
|
|
@ -91,7 +93,7 @@ const StreamsList = memo(
|
|||
<View style={styles.sectionLoadingIndicator}>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
<Text style={[styles.sectionLoadingText, { color: colors.primary }]}>
|
||||
Loading...
|
||||
{t('common.loading')}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -157,21 +159,21 @@ const StreamsList = memo(
|
|||
<View style={styles.autoplayOverlay}>
|
||||
<View style={styles.autoplayIndicator}>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
<Text style={styles.autoplayText}>Starting best stream...</Text>
|
||||
<Text style={styles.autoplayText}>{t('streams.starting_best_stream')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}, [isAutoplayWaiting, autoplayTriggered, styles, colors.primary]);
|
||||
}, [isAutoplayWaiting, autoplayTriggered, styles, colors.primary, t]);
|
||||
|
||||
const ListFooterComponent = useMemo(() => {
|
||||
if (!(loadingStreams || loadingEpisodeStreams) || !hasStremioStreamProviders) return null;
|
||||
return (
|
||||
<View style={styles.footerLoading}>
|
||||
<ActivityIndicator size="small" color={colors.primary} />
|
||||
<Text style={styles.footerLoadingText}>Loading more sources...</Text>
|
||||
<Text style={styles.footerLoadingText}>{t('streams.loading_more_sources')}</Text>
|
||||
</View>
|
||||
);
|
||||
}, [loadingStreams, loadingEpisodeStreams, hasStremioStreamProviders, styles, colors.primary]);
|
||||
}, [loadingStreams, loadingEpisodeStreams, hasStremioStreamProviders, styles, colors.primary, t]);
|
||||
|
||||
return (
|
||||
<View collapsable={false} style={{ flex: 1 }}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue