mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +00:00
Merge c47eeb5223 into cbc9fc4fa6
This commit is contained in:
commit
46f2db7289
17 changed files with 464 additions and 180 deletions
|
|
@ -16,6 +16,7 @@ import QualityBadge from './metadata/QualityBadge';
|
|||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useDownloads } from '../contexts/DownloadsContext';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface StreamCardProps {
|
||||
stream: Stream;
|
||||
|
|
@ -63,6 +64,7 @@ const StreamCard = memo(({
|
|||
const { settings } = useSettings();
|
||||
const { startDownload } = useDownloads();
|
||||
const { showSuccess, showInfo } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Handle long press to copy stream URL to clipboard
|
||||
const handleLongPress = useCallback(async () => {
|
||||
|
|
@ -72,10 +74,10 @@ const StreamCard = memo(({
|
|||
|
||||
// Use toast for Android, custom alert for iOS
|
||||
if (Platform.OS === 'android') {
|
||||
showSuccess('URL Copied', 'Stream URL copied to clipboard!');
|
||||
showSuccess(t('stream_card.url_copied'), t('stream_card.url_copied_msg'));
|
||||
} else {
|
||||
// iOS uses custom alert
|
||||
showAlert('Copied!', 'Stream URL has been copied to clipboard.');
|
||||
showAlert(t('stream_card.copied'), t('stream_card.copied_msg'));
|
||||
}
|
||||
} catch (error) {
|
||||
// Fallback: show URL in alert if clipboard fails
|
||||
|
|
@ -131,7 +133,7 @@ const StreamCard = memo(({
|
|||
try {
|
||||
const downloadsModule = require('../contexts/DownloadsContext');
|
||||
if (downloadsModule && downloadsModule.isDownloadingUrl && downloadsModule.isDownloadingUrl(url)) {
|
||||
showAlert('Already Downloading', 'This download has already started for this exact link.');
|
||||
showAlert(t('stream_card.already_downloading'), t('stream_card.already_downloading_msg'));
|
||||
return;
|
||||
}
|
||||
} catch { }
|
||||
|
|
@ -178,9 +180,9 @@ const StreamCard = memo(({
|
|||
imdbId: parentImdbId || parent.imdbId || undefined,
|
||||
tmdbId: tmdbId,
|
||||
});
|
||||
showAlert('Download Started', 'Your download has been added to the queue.');
|
||||
showAlert(t('stream_card.download_started'), t('stream_card.download_started_msg'));
|
||||
} catch (e: any) {
|
||||
showAlert('Download Failed', e.message || 'Could not start download.');
|
||||
showAlert(t('stream_card.download_failed'), e.message || t('stream_card.download_failed'));
|
||||
}
|
||||
}, [startDownload, stream.url, stream.headers, streamInfo.quality, showAlert, stream.name, stream.title, parentId, parentImdbId, parentTitle, parentType, parentSeason, parentEpisode, parentEpisodeTitle, parentPosterUrl, providerName]);
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { MaterialIcons } from '@expo/vector-icons';
|
|||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { MalApiService } from '../../services/mal/MalApi';
|
||||
import { MalListStatus, MalAnimeNode } from '../../types/mal';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
|
||||
interface MalEditModalProps {
|
||||
|
|
@ -33,6 +34,7 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
|
|||
onUpdateSuccess,
|
||||
}) => {
|
||||
const { currentTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { showSuccess, showError } = useToast();
|
||||
|
||||
const [status, setStatus] = useState<MalListStatus>(anime.list_status.status);
|
||||
|
|
@ -62,11 +64,11 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
|
|||
|
||||
await MalApiService.updateStatus(anime.node.id, status, epNum, scoreNum, isRewatching);
|
||||
|
||||
showSuccess('Updated', `${anime.node.title} status updated on MAL`);
|
||||
showSuccess(t('mal.updated'), t('mal.updated_msg', { title: anime.node.title }));
|
||||
onUpdateSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
showError('Update Failed', 'Could not update MAL status');
|
||||
showError(t('mal.update_failed'), t('mal.update_failed_msg'));
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
|
|
@ -74,22 +76,22 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
|
|||
|
||||
const handleRemove = async () => {
|
||||
Alert.alert(
|
||||
'Remove from List',
|
||||
`Are you sure you want to remove ${anime.node.title} from your MyAnimeList?`,
|
||||
t('mal.remove_title'),
|
||||
t('mal.remove_confirm', { title: anime.node.title }),
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Remove',
|
||||
{ text: t('common.cancel'), style: 'cancel' },
|
||||
{
|
||||
text: t('common.delete'),
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
setIsRemoving(true);
|
||||
try {
|
||||
await MalApiService.removeFromList(anime.node.id);
|
||||
showSuccess('Removed', `${anime.node.title} removed from MAL`);
|
||||
showSuccess(t('mal.removed'), t('mal.removed_msg', { title: anime.node.title }));
|
||||
onUpdateSuccess();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
showError('Remove Failed', 'Could not remove from MAL');
|
||||
showError(t('mal.remove_failed'), t('mal.remove_failed_msg'));
|
||||
} finally {
|
||||
setIsRemoving(false);
|
||||
}
|
||||
|
|
@ -100,11 +102,11 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
|
|||
};
|
||||
|
||||
const statusOptions: { label: string; value: MalListStatus }[] = [
|
||||
{ label: 'Watching', value: 'watching' },
|
||||
{ label: 'Completed', value: 'completed' },
|
||||
{ label: 'On Hold', value: 'on_hold' },
|
||||
{ label: 'Dropped', value: 'dropped' },
|
||||
{ label: 'Plan to Watch', value: 'plan_to_watch' },
|
||||
{ label: t('mal.watching'), value: 'watching' },
|
||||
{ label: t('mal.completed'), value: 'completed' },
|
||||
{ label: t('mal.on_hold'), value: 'on_hold' },
|
||||
{ label: t('mal.dropped'), value: 'dropped' },
|
||||
{ label: t('mal.plan_to_watch'), value: 'plan_to_watch' },
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -122,7 +124,7 @@ export const MalEditModal: React.FC<MalEditModalProps> = ({
|
|||
<View style={[styles.modalContent, { backgroundColor: currentTheme.colors.darkGray || '#1A1A1A' }]}>
|
||||
<View style={styles.header}>
|
||||
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]} numberOfLines={1}>
|
||||
Edit {anime.node.title}
|
||||
{t('mal.edit_title', { title: anime.node.title })}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={onClose}>
|
||||
<MaterialIcons name="close" size={24} color={currentTheme.colors.mediumEmphasis} />
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ const TrailerModal: React.FC<TrailerModalProps> = memo(({
|
|||
}, [onClose, resumeTrailer]);
|
||||
|
||||
const handleTrailerError = useCallback(() => {
|
||||
setError('Failed to play trailer');
|
||||
setError(t('trailers.error_play'));
|
||||
setIsPlaying(false);
|
||||
}, []);
|
||||
|
||||
|
|
@ -171,7 +171,7 @@ const TrailerModal: React.FC<TrailerModalProps> = memo(({
|
|||
|
||||
if (isUnsupportedIosMediaFormat(error)) {
|
||||
logger.error('TrailerModal', 'Unsupported iOS trailer format:', error);
|
||||
setError('This trailer format is not supported on iOS.');
|
||||
setError(t('trailers.error_ios_format'));
|
||||
setLoading(false);
|
||||
setIsPlaying(false);
|
||||
return;
|
||||
|
|
@ -187,7 +187,7 @@ const TrailerModal: React.FC<TrailerModalProps> = memo(({
|
|||
}
|
||||
|
||||
logger.error('TrailerModal', 'Video error after retries:', error);
|
||||
setError('Unable to play trailer. Please try again.');
|
||||
setError(t('trailers.error_retry'));
|
||||
setLoading(false);
|
||||
}, [retryCount, loadTrailer, trailer?.key]);
|
||||
|
||||
|
|
|
|||
|
|
@ -176,7 +176,12 @@
|
|||
"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"
|
||||
"removed_from_collection_desc": "Removed from your Trakt collection",
|
||||
"sync_failed": "Sync Failed",
|
||||
"sync_connect_first": "Please connect to Trakt first",
|
||||
"sync_complete": "Sync Complete",
|
||||
"sync_up_to_date": "Library is up to date",
|
||||
"sync_error": "Unable to sync. Please try again."
|
||||
},
|
||||
"metadata": {
|
||||
"unable_to_load": "Unable to Load Content",
|
||||
|
|
@ -275,7 +280,9 @@
|
|||
"removed_from_collection_hero": "Removed from Collection",
|
||||
"removed_from_collection_desc_hero": "Removed from your Trakt collection",
|
||||
"mark_as_watched": "Mark as Watched",
|
||||
"mark_as_unwatched": "Mark as Unwatched"
|
||||
"mark_as_unwatched": "Mark as Unwatched",
|
||||
"no_backdrops": "No backdrops found",
|
||||
"error_backdrops": "Failed to load backdrops"
|
||||
},
|
||||
"cast": {
|
||||
"biography": "Biography",
|
||||
|
|
@ -334,7 +341,10 @@
|
|||
"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"
|
||||
"watch_on_youtube": "Watch on YouTube",
|
||||
"error_play": "Failed to play trailer",
|
||||
"error_ios_format": "This trailer format is not supported on iOS.",
|
||||
"error_retry": "Unable to play trailer. Please try again."
|
||||
},
|
||||
"catalog": {
|
||||
"no_content_found": "No content found",
|
||||
|
|
@ -360,7 +370,9 @@
|
|||
"still_fetching": "Still fetching streams…",
|
||||
"no_streams_available": "No streams available",
|
||||
"starting_best_stream": "Starting best stream...",
|
||||
"loading_more_sources": "Loading more sources..."
|
||||
"loading_more_sources": "Loading more sources...",
|
||||
"torrent_not_supported": "Not supported",
|
||||
"torrent_not_supported_msg": "Torrent streaming is not supported yet."
|
||||
},
|
||||
"player_ui": {
|
||||
"via": "via {{name}}",
|
||||
|
|
@ -469,13 +481,13 @@
|
|||
"profanity": "Profanity",
|
||||
"alcohol": "Alcohol/Drugs",
|
||||
"frightening": "Frightening"
|
||||
},
|
||||
},
|
||||
"severity": {
|
||||
"severe": "Severe",
|
||||
"moderate": "Moderate",
|
||||
"mild": "Mild",
|
||||
"none": "None"
|
||||
}
|
||||
}
|
||||
},
|
||||
"addons": {
|
||||
"title": "Addons",
|
||||
|
|
@ -542,7 +554,12 @@
|
|||
"sign_out_error": "Failed to sign out of Trakt.",
|
||||
"sync_complete_title": "Sync Complete",
|
||||
"sync_success_msg": "Successfully synced your watch progress with Trakt.",
|
||||
"sync_error_msg": "Sync failed. Please try again."
|
||||
"sync_error_msg": "Sync failed. Please try again.",
|
||||
"conflict_title": "Conflict",
|
||||
"conflict_simkl_msg": "You cannot connect to Trakt while Simkl is connected. Please disconnect Simkl first.",
|
||||
"library_sync_updated": "Library Sync Mode Updated",
|
||||
"library_sync_updated_msg": "Trakt library sync is now set to: {{mode}}",
|
||||
"library_sync_error": "Failed to update library sync mode"
|
||||
},
|
||||
"simkl": {
|
||||
"title": "Simkl Settings",
|
||||
|
|
@ -707,7 +724,9 @@
|
|||
"media": "MEDIA",
|
||||
"notifications": "NOTIFICATIONS",
|
||||
"testing": "TESTING",
|
||||
"danger_zone": "DANGER ZONE"
|
||||
"danger_zone": "DANGER ZONE",
|
||||
"data": "DATA",
|
||||
"general": "GENERAL"
|
||||
},
|
||||
"items": {
|
||||
"legal": "Legal & Disclaimer",
|
||||
|
|
@ -840,7 +859,18 @@
|
|||
"env_warning": "Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to enable sync."
|
||||
},
|
||||
"connection": "Connection"
|
||||
}
|
||||
},
|
||||
"clear_mdblist_cache_title": "Clear MDBList Cache",
|
||||
"clear_mdblist_cache_confirm": "Are you sure you want to clear all cached MDBList data? This cannot be undone.",
|
||||
"clear_mdblist_cache_success": "MDBList cache has been cleared.",
|
||||
"clear_mdblist_cache_error": "Could not clear MDBList cache.",
|
||||
"reset_onboarding_success": "Onboarding has been reset. Restart the app to see the onboarding flow.",
|
||||
"reset_onboarding_error": "Failed to reset onboarding.",
|
||||
"reset_campaigns_success": "Campaign history reset. Restart app to see posters again.",
|
||||
"clear_data_success": "All data cleared. Please restart the app.",
|
||||
"clear_data_error": "Failed to clear data.",
|
||||
"plugin_tester": "Plugin Tester",
|
||||
"plugin_tester_desc": "Run a plugin and inspect logs/streams"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Privacy & Data",
|
||||
|
|
@ -915,7 +945,8 @@
|
|||
"confirm_remove_title": "Remove API Key",
|
||||
"confirm_remove_msg": "Are you sure you want to remove your OpenRouter API key? This will disable AI chat features.",
|
||||
"success_removed": "API key removed successfully",
|
||||
"error_remove": "Failed to remove API key"
|
||||
"error_remove": "Failed to remove API key",
|
||||
"error_load_context": "Failed to load content details for AI chat"
|
||||
},
|
||||
"catalog_settings": {
|
||||
"title": "Catalogs",
|
||||
|
|
@ -1378,7 +1409,20 @@
|
|||
"got_it": "Got it!",
|
||||
"repo_format_hint": "Format: https://raw.githubusercontent.com/username/repo/refs/heads/branch",
|
||||
"cancel": "Cancel",
|
||||
"add": "Add"
|
||||
"add": "Add",
|
||||
"error_update_extensions": "Failed to update extensions",
|
||||
"error_valid_url": "Please enter a valid repository URL",
|
||||
"error_valid_url_format": "Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/user/repo/refs/heads/branch",
|
||||
"error_update_repo": "Failed to update repository",
|
||||
"error_remove_repo": "Failed to remove repository",
|
||||
"error_save_repo": "Failed to save repository URL",
|
||||
"error_set_repo_first": "Please set a repository URL first",
|
||||
"error_repo": "Repository Error",
|
||||
"error_update_status": "Failed to update extension status",
|
||||
"error_clear_extensions": "Failed to clear extensions",
|
||||
"error_clear_cache": "Failed to clear repository cache",
|
||||
"showbox_saved": "Saved",
|
||||
"showbox_saved_msg": "ShowBox settings updated"
|
||||
},
|
||||
"theme": {
|
||||
"title": "App Themes",
|
||||
|
|
@ -1504,5 +1548,84 @@
|
|||
"provider_logs": "Provider Logs",
|
||||
"no_logs_captured": "No logs captured."
|
||||
}
|
||||
},
|
||||
"mal": {
|
||||
"title": "MyAnimeList",
|
||||
"connect_title": "Connect MyAnimeList",
|
||||
"connect_desc": "Sync your watch history and manage your anime list.",
|
||||
"sign_in": "Sign In with MAL",
|
||||
"sign_out": "Sign Out",
|
||||
"sign_out_confirm": "Are you sure you want to disconnect?",
|
||||
"sync_button": "Sync",
|
||||
"connected": "Connected to MyAnimeList",
|
||||
"error_connect": "Failed to connect to MyAnimeList",
|
||||
"error_sign_in": "An error occurred during sign in: {{message}}",
|
||||
"sync_complete": "Sync Complete",
|
||||
"sync_complete_msg": "MAL data has been refreshed.",
|
||||
"sync_failed": "Sync Failed",
|
||||
"sync_failed_msg": "Could not refresh MAL data.",
|
||||
"sync_settings": "Sync Settings",
|
||||
"enable_sync": "Enable MAL Sync",
|
||||
"enable_sync_desc": "Global switch to enable or disable all MyAnimeList features.",
|
||||
"auto_episode": "Auto Episode Update",
|
||||
"auto_episode_desc": "Automatically update your progress on MAL when you finish watching an episode (>=90% completion).",
|
||||
"auto_add": "Auto Add Anime",
|
||||
"auto_add_desc": "If an anime is not in your MAL list, it will be added automatically when you start watching.",
|
||||
"auto_sync_library": "Auto-Sync to Library",
|
||||
"auto_sync_library_desc": "Automatically add items from your MAL 'Watching' list to your Nuvio Library.",
|
||||
"include_nsfw": "Include NSFW Content",
|
||||
"include_nsfw_desc": "Allow NSFW entries to be returned when fetching your MAL list.",
|
||||
"stat_total": "Total",
|
||||
"stat_days": "Days",
|
||||
"stat_mean": "Mean",
|
||||
"watching": "Watching",
|
||||
"completed": "Completed",
|
||||
"on_hold": "On Hold",
|
||||
"dropped": "Dropped",
|
||||
"plan_to_watch": "Plan to Watch",
|
||||
"edit_title": "Edit {{title}}",
|
||||
"updated": "Updated",
|
||||
"updated_msg": "{{title}} status updated on MAL",
|
||||
"update_failed": "Update Failed",
|
||||
"update_failed_msg": "Could not update MAL status",
|
||||
"remove_title": "Remove from List",
|
||||
"remove_confirm": "Are you sure you want to remove {{title}} from your MyAnimeList?",
|
||||
"removed": "Removed",
|
||||
"removed_msg": "{{title}} removed from MAL",
|
||||
"remove_failed": "Remove Failed",
|
||||
"remove_failed_msg": "Could not remove from MAL",
|
||||
"description": "Sync with MyAnimeList",
|
||||
"discord_open_prompt": "Discord: @{{username}}\n\nDo you want to open Discord and search for this user?"
|
||||
},
|
||||
"profiles": {
|
||||
"error_load": "Failed to load profiles",
|
||||
"error_save": "Failed to save profiles",
|
||||
"error_empty_name": "Please enter a profile name",
|
||||
"error_delete_active": "Cannot delete the active profile. Switch to another profile first.",
|
||||
"error_delete_only": "Cannot delete the only profile",
|
||||
"delete_title": "Delete Profile",
|
||||
"delete_confirm": "Are you sure you want to delete this profile? This action cannot be undone."
|
||||
},
|
||||
"auth": {
|
||||
"invalid_email": "Invalid Email",
|
||||
"invalid_email_msg": "Enter a valid email address",
|
||||
"password_short": "Password Too Short",
|
||||
"password_short_msg": "Password must be at least 6 characters",
|
||||
"passwords_mismatch": "Passwords Don't Match",
|
||||
"passwords_mismatch_msg": "Passwords do not match",
|
||||
"auth_failed": "Authentication Failed",
|
||||
"login_success": "Logged in successfully",
|
||||
"signup_success": "Sign up successful"
|
||||
},
|
||||
"stream_card": {
|
||||
"url_copied": "URL Copied",
|
||||
"url_copied_msg": "Stream URL copied to clipboard!",
|
||||
"copied": "Copied!",
|
||||
"copied_msg": "Stream URL has been copied to clipboard.",
|
||||
"already_downloading": "Already Downloading",
|
||||
"already_downloading_msg": "This download has already started for this exact link.",
|
||||
"download_started": "Download Started",
|
||||
"download_started_msg": "Your download has been added to the queue.",
|
||||
"download_failed": "Download Failed"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -176,7 +176,12 @@
|
|||
"added_to_collection": "Ajouté à la collection",
|
||||
"added_to_collection_desc": "Ajouté à votre collection Trakt",
|
||||
"removed_from_collection": "Retiré de la collection",
|
||||
"removed_from_collection_desc": "Retiré de votre collection Trakt"
|
||||
"removed_from_collection_desc": "Retiré de votre collection Trakt",
|
||||
"sync_failed": "Échec de la synchronisation",
|
||||
"sync_connect_first": "Connecte-toi d'abord à Trakt",
|
||||
"sync_complete": "Synchronisation terminée",
|
||||
"sync_up_to_date": "La bibliothèque est à jour",
|
||||
"sync_error": "Impossible de synchroniser. Réessaie."
|
||||
},
|
||||
"metadata": {
|
||||
"unable_to_load": "Impossible de charger le contenu",
|
||||
|
|
@ -275,7 +280,9 @@
|
|||
"removed_from_collection_hero": "Retiré de la collection",
|
||||
"removed_from_collection_desc_hero": "Retiré de votre collection Trakt",
|
||||
"mark_as_watched": "Marquer comme vu",
|
||||
"mark_as_unwatched": "Marquer comme non vu"
|
||||
"mark_as_unwatched": "Marquer comme non vu",
|
||||
"no_backdrops": "Aucun fond d'écran trouvé",
|
||||
"error_backdrops": "Échec du chargement des fonds d'écran"
|
||||
},
|
||||
"cast": {
|
||||
"biography": "Biographie",
|
||||
|
|
@ -334,7 +341,10 @@
|
|||
"unavailable": "Bande-annonce indisponible",
|
||||
"unavailable_desc": "Cette bande-annonce n'a pas pu être chargée pour le moment. Veuillez réessayer plus tard.",
|
||||
"unable_to_play": "Impossible de lire la bande-annonce. Veuillez réessayer.",
|
||||
"watch_on_youtube": "Regarder sur YouTube"
|
||||
"watch_on_youtube": "Regarder sur YouTube",
|
||||
"error_play": "Échec de la lecture de la bande-annonce",
|
||||
"error_ios_format": "Ce format de bande-annonce n'est pas supporté sur iOS.",
|
||||
"error_retry": "Impossible de lire la bande-annonce. Réessaie."
|
||||
},
|
||||
"catalog": {
|
||||
"no_content_found": "Aucun contenu trouvé",
|
||||
|
|
@ -360,7 +370,9 @@
|
|||
"still_fetching": "Récupération des flux en cours...",
|
||||
"no_streams_available": "Aucun flux disponible",
|
||||
"starting_best_stream": "Démarrage du meilleur flux...",
|
||||
"loading_more_sources": "Chargement d'autres sources..."
|
||||
"loading_more_sources": "Chargement d'autres sources...",
|
||||
"torrent_not_supported": "Non supporté",
|
||||
"torrent_not_supported_msg": "Le streaming par torrent n'est pas encore supporté."
|
||||
},
|
||||
"player_ui": {
|
||||
"via": "via {{name}}",
|
||||
|
|
@ -419,7 +431,12 @@
|
|||
"timing_offset": "Décalage temporel (s)",
|
||||
"visual_sync": "Synchronisation visuelle",
|
||||
"timing_hint": "Avancez (-) ou retardez (+) les sous-titres pour synchroniser si nécessaire.",
|
||||
"reset_defaults": "Réinitialiser les paramètres"
|
||||
"reset_defaults": "Réinitialiser les paramètres",
|
||||
"mark_intro_start": "Marquer le début de l'intro",
|
||||
"mark_intro_end": "Marquer la fin de l'intro",
|
||||
"intro_start_marked": "Début de l'intro marqué",
|
||||
"intro_submitted": "Intro soumise avec succès",
|
||||
"intro_submit_failed": "Échec de la soumission de l'intro"
|
||||
},
|
||||
"downloads": {
|
||||
"title": "Téléchargements",
|
||||
|
|
@ -522,7 +539,12 @@
|
|||
"sign_out_error": "Échec de la déconnexion de Trakt.",
|
||||
"sync_complete_title": "Synchronisation terminée",
|
||||
"sync_success_msg": "Votre progression a été synchronisée avec succès avec Trakt.",
|
||||
"sync_error_msg": "La synchronisation a échoué. Veuillez réessayer."
|
||||
"sync_error_msg": "La synchronisation a échoué. Veuillez réessayer.",
|
||||
"conflict_title": "Conflit",
|
||||
"conflict_simkl_msg": "Tu ne peux pas connecter Trakt tant que Simkl est connecté. Déconnecte d'abord Simkl.",
|
||||
"library_sync_updated": "Mode de synchronisation mis à jour",
|
||||
"library_sync_updated_msg": "La synchronisation Trakt est maintenant réglée sur : {{mode}}",
|
||||
"library_sync_error": "Échec de la mise à jour du mode de synchronisation"
|
||||
},
|
||||
"simkl": {
|
||||
"title": "Paramètres Simkl",
|
||||
|
|
@ -685,7 +707,9 @@
|
|||
"media": "MÉDIA",
|
||||
"notifications": "NOTIFICATIONS",
|
||||
"testing": "TESTS",
|
||||
"danger_zone": "ZONE DE DANGER"
|
||||
"danger_zone": "ZONE DE DANGER",
|
||||
"data": "DONNÉES",
|
||||
"general": "GÉNÉRAL"
|
||||
},
|
||||
"items": {
|
||||
"legal": "Mentions Légales",
|
||||
|
|
@ -818,7 +842,20 @@
|
|||
"env_warning": "Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY to enable sync."
|
||||
},
|
||||
"connection": "Connection"
|
||||
}
|
||||
},
|
||||
"clear_mdblist_cache_title": "Effacer le cache MDBList",
|
||||
"clear_mdblist_cache_confirm": "Es-tu sûr de vouloir effacer toutes les données MDBList en cache ? Cette action est irréversible.",
|
||||
"clear_mdblist_cache_success": "Le cache MDBList a été effacé.",
|
||||
"clear_mdblist_cache_error": "Impossible d'effacer le cache MDBList.",
|
||||
"reset_onboarding_success": "L'accueil a été réinitialisé. Redémarre l'application pour voir le flux d'accueil.",
|
||||
"reset_onboarding_error": "Échec de la réinitialisation de l'accueil.",
|
||||
"reset_campaigns_success": "Historique des campagnes réinitialisé. Redémarre l'application pour revoir les affiches.",
|
||||
"clear_data_success": "Toutes les données ont été effacées. Redémarre l'application.",
|
||||
"clear_data_error": "Échec de l'effacement des données.",
|
||||
"plugin_tester": "Testeur de plugin",
|
||||
"plugin_tester_desc": "Exécuter un plugin et inspecter les logs/flux",
|
||||
"vietnamese": "Vietnamien",
|
||||
"backup_restore_desc": "Créer et restaurer des sauvegardes"
|
||||
},
|
||||
"privacy": {
|
||||
"title": "Confidentialité et Données",
|
||||
|
|
@ -893,7 +930,8 @@
|
|||
"confirm_remove_title": "Supprimer la clé API",
|
||||
"confirm_remove_msg": "Êtes-vous sûr de vouloir supprimer votre clé API OpenRouter ? Cela désactivera les fonctionnalités de chat IA.",
|
||||
"success_removed": "Clé API supprimée avec succès",
|
||||
"error_remove": "Échec de la suppression de la clé API"
|
||||
"error_remove": "Échec de la suppression de la clé API",
|
||||
"error_load_context": "Échec du chargement des détails pour le chat IA"
|
||||
},
|
||||
"catalog_settings": {
|
||||
"title": "Catalogues",
|
||||
|
|
@ -1069,7 +1107,9 @@
|
|||
"error_load": "Échec du chargement des catalogues",
|
||||
"movies": "Films",
|
||||
"tv_shows": "Séries TV"
|
||||
}
|
||||
},
|
||||
"prefer_external_meta": "Préférer les métadonnées externes",
|
||||
"prefer_external_meta_desc": "Utiliser les métadonnées externes sur la page de détails"
|
||||
},
|
||||
"calendar": {
|
||||
"title": "Calendrier",
|
||||
|
|
@ -1354,7 +1394,20 @@
|
|||
"got_it": "Compris !",
|
||||
"repo_format_hint": "Format : https://raw.githubusercontent.com/user/repo/branch",
|
||||
"cancel": "Annuler",
|
||||
"add": "Ajouter"
|
||||
"add": "Ajouter",
|
||||
"error_update_extensions": "Échec de la mise à jour des extensions",
|
||||
"error_valid_url": "Entre une URL de dépôt valide",
|
||||
"error_valid_url_format": "Utilise une URL GitHub raw valide :\n\nhttps://raw.githubusercontent.com/user/repo/refs/heads/branch",
|
||||
"error_update_repo": "Échec de la mise à jour du dépôt",
|
||||
"error_remove_repo": "Échec de la suppression du dépôt",
|
||||
"error_save_repo": "Échec de l'enregistrement de l'URL du dépôt",
|
||||
"error_set_repo_first": "Définis d'abord une URL de dépôt",
|
||||
"error_repo": "Erreur de dépôt",
|
||||
"error_update_status": "Échec de la mise à jour du statut de l'extension",
|
||||
"error_clear_extensions": "Échec de la suppression des extensions",
|
||||
"error_clear_cache": "Échec du vidage du cache du dépôt",
|
||||
"showbox_saved": "Enregistré",
|
||||
"showbox_saved_msg": "Paramètres ShowBox mis à jour"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Thèmes de l'App",
|
||||
|
|
@ -1480,5 +1533,99 @@
|
|||
"provider_logs": "Logs du Fournisseur",
|
||||
"no_logs_captured": "Aucun log capturé."
|
||||
}
|
||||
},
|
||||
"mal": {
|
||||
"title": "MyAnimeList",
|
||||
"connect_title": "Connecter MyAnimeList",
|
||||
"connect_desc": "Synchronise ton historique et gère ta liste d'anime.",
|
||||
"sign_in": "Se connecter avec MAL",
|
||||
"sign_out": "Se déconnecter",
|
||||
"sign_out_confirm": "Es-tu sûr de vouloir te déconnecter ?",
|
||||
"sync_button": "Synchroniser",
|
||||
"connected": "Connecté à MyAnimeList",
|
||||
"error_connect": "Échec de la connexion à MyAnimeList",
|
||||
"error_sign_in": "Une erreur est survenue lors de la connexion : {{message}}",
|
||||
"sync_complete": "Synchronisation terminée",
|
||||
"sync_complete_msg": "Les données MAL ont été actualisées.",
|
||||
"sync_failed": "Échec de la synchronisation",
|
||||
"sync_failed_msg": "Impossible d'actualiser les données MAL.",
|
||||
"sync_settings": "Paramètres de synchronisation",
|
||||
"enable_sync": "Activer la synchronisation MAL",
|
||||
"enable_sync_desc": "Interrupteur global pour activer ou désactiver toutes les fonctionnalités MyAnimeList.",
|
||||
"auto_episode": "Mise à jour auto des épisodes",
|
||||
"auto_episode_desc": "Met automatiquement à jour ta progression sur MAL quand tu finis un épisode (>=90% de complétion).",
|
||||
"auto_add": "Ajout auto des anime",
|
||||
"auto_add_desc": "Si un anime n'est pas dans ta liste MAL, il sera ajouté automatiquement quand tu commences à le regarder.",
|
||||
"auto_sync_library": "Synchronisation auto vers la bibliothèque",
|
||||
"auto_sync_library_desc": "Ajoute automatiquement les éléments de ta liste MAL \"En cours\" à ta bibliothèque Nuvio.",
|
||||
"include_nsfw": "Inclure le contenu NSFW",
|
||||
"include_nsfw_desc": "Autoriser les entrées NSFW lors de la récupération de ta liste MAL.",
|
||||
"stat_total": "Total",
|
||||
"stat_days": "Jours",
|
||||
"stat_mean": "Moyenne",
|
||||
"watching": "En cours",
|
||||
"completed": "Terminé",
|
||||
"on_hold": "En pause",
|
||||
"dropped": "Abandonné",
|
||||
"plan_to_watch": "À regarder",
|
||||
"edit_title": "Modifier {{title}}",
|
||||
"updated": "Mis à jour",
|
||||
"updated_msg": "Statut de {{title}} mis à jour sur MAL",
|
||||
"update_failed": "Échec de la mise à jour",
|
||||
"update_failed_msg": "Impossible de mettre à jour le statut MAL",
|
||||
"remove_title": "Retirer de la liste",
|
||||
"remove_confirm": "Es-tu sûr de vouloir retirer {{title}} de ta liste MyAnimeList ?",
|
||||
"removed": "Retiré",
|
||||
"removed_msg": "{{title}} retiré de MAL",
|
||||
"remove_failed": "Échec du retrait",
|
||||
"remove_failed_msg": "Impossible de retirer de MAL",
|
||||
"description": "Synchroniser avec MyAnimeList",
|
||||
"discord_open_prompt": "Discord : @{{username}}\n\nOuvrir Discord et chercher cet utilisateur ?"
|
||||
},
|
||||
"profiles": {
|
||||
"error_load": "Échec du chargement des profils",
|
||||
"error_save": "Échec de l'enregistrement des profils",
|
||||
"error_empty_name": "Entre un nom de profil",
|
||||
"error_delete_active": "Impossible de supprimer le profil actif. Bascule d'abord sur un autre profil.",
|
||||
"error_delete_only": "Impossible de supprimer le seul profil",
|
||||
"delete_title": "Supprimer le profil",
|
||||
"delete_confirm": "Es-tu sûr de vouloir supprimer ce profil ? Cette action est irréversible."
|
||||
},
|
||||
"auth": {
|
||||
"invalid_email": "E-mail invalide",
|
||||
"invalid_email_msg": "Entre une adresse e-mail valide",
|
||||
"password_short": "Mot de passe trop court",
|
||||
"password_short_msg": "Le mot de passe doit contenir au moins 6 caractères",
|
||||
"passwords_mismatch": "Mots de passe différents",
|
||||
"passwords_mismatch_msg": "Les mots de passe ne correspondent pas",
|
||||
"auth_failed": "Échec de l'authentification",
|
||||
"login_success": "Connexion réussie",
|
||||
"signup_success": "Inscription réussie"
|
||||
},
|
||||
"stream_card": {
|
||||
"url_copied": "URL copiée",
|
||||
"url_copied_msg": "URL du flux copiée dans le presse-papiers !",
|
||||
"copied": "Copié !",
|
||||
"copied_msg": "L'URL du flux a été copiée dans le presse-papiers.",
|
||||
"already_downloading": "Déjà en téléchargement",
|
||||
"already_downloading_msg": "Ce téléchargement a déjà été lancé pour ce lien.",
|
||||
"download_started": "Téléchargement lancé",
|
||||
"download_started_msg": "Ton téléchargement a été ajouté à la file d'attente.",
|
||||
"download_failed": "Échec du téléchargement"
|
||||
},
|
||||
"parentalGuide": {
|
||||
"labels": {
|
||||
"nudity": "Nudité",
|
||||
"violence": "Violence",
|
||||
"profanity": "Langage grossier",
|
||||
"alcohol": "Alcool/Drogues",
|
||||
"frightening": "Scènes effrayantes"
|
||||
},
|
||||
"severity": {
|
||||
"severe": "Grave",
|
||||
"moderate": "Modéré",
|
||||
"mild": "Léger",
|
||||
"none": "Aucun"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
import CustomAlert from '../components/CustomAlert';
|
||||
import { useRoute, useNavigation, RouteProp, useFocusEffect } from '@react-navigation/native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import FastImage from '@d11/react-native-fast-image';
|
||||
import { BlurView as ExpoBlurView } from 'expo-blur';
|
||||
|
|
@ -428,6 +429,7 @@ const SuggestionChip: React.FC<SuggestionChipProps> = React.memo(({ text, onPres
|
|||
}, (prev, next) => prev.text === next.text && prev.onPress === next.onPress && prev.index === next.index);
|
||||
|
||||
const AIChatScreen: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
// CustomAlert state
|
||||
const [alertVisible, setAlertVisible] = useState(false);
|
||||
const [alertTitle, setAlertTitle] = useState('');
|
||||
|
|
@ -597,7 +599,7 @@ const AIChatScreen: React.FC = () => {
|
|||
}
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('Error loading context:', error);
|
||||
openAlert('Error', 'Failed to load content details for AI chat');
|
||||
openAlert(t('common.error'), t('ai_settings.error_load_context'));
|
||||
} finally {
|
||||
setIsLoadingContext(false);
|
||||
{/* CustomAlert at root */ }
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useNavigation, useRoute } from '@react-navigation/native';
|
|||
import * as Haptics from 'expo-haptics';
|
||||
import { useToast } from '../contexts/ToastContext';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const EMAIL_CONFIRMATION_REQUIRED_PREFIX = '__EMAIL_CONFIRMATION__';
|
||||
const AUTH_BG_GRADIENT = ['#07090F', '#0D1020', '#140B24'] as const;
|
||||
|
|
@ -51,6 +52,7 @@ const AuthScreen: React.FC = () => {
|
|||
const safeTopInset = Math.max(insets.top, Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : 0);
|
||||
const backButtonTop = safeTopInset + 8;
|
||||
const { showError, showSuccess } = useToast();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
|
|
@ -163,21 +165,21 @@ const AuthScreen: React.FC = () => {
|
|||
if (!isEmailValid) {
|
||||
const msg = 'Enter a valid email address';
|
||||
setError(msg);
|
||||
showError('Invalid Email', 'Enter a valid email address');
|
||||
showError(t('auth.invalid_email'), t('auth.invalid_email_msg'));
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (!isPasswordValid) {
|
||||
const msg = 'Password must be at least 6 characters';
|
||||
setError(msg);
|
||||
showError('Password Too Short', 'Password must be at least 6 characters');
|
||||
showError(t('auth.password_short'), t('auth.password_short_msg'));
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (mode === 'signup' && !passwordsMatch) {
|
||||
const msg = 'Passwords do not match';
|
||||
setError(msg);
|
||||
showError('Passwords Don\'t Match', 'Passwords do not match');
|
||||
showError(t('auth.passwords_mismatch'), t('auth.passwords_mismatch_msg'));
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
|
@ -197,11 +199,11 @@ const AuthScreen: React.FC = () => {
|
|||
|
||||
const cleanError = normalizeAuthErrorMessage(err);
|
||||
setError(cleanError);
|
||||
showError('Authentication Failed', cleanError);
|
||||
showError(t('auth.auth_failed'), cleanError);
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error).catch(() => {});
|
||||
} else {
|
||||
const msg = mode === 'signin' ? 'Logged in successfully' : 'Sign up successful';
|
||||
showSuccess('Success', msg);
|
||||
const msg = mode === 'signin' ? t('auth.login_success') : t('auth.signup_success');
|
||||
showSuccess(t('common.success'), msg);
|
||||
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success).catch(() => {});
|
||||
|
||||
// Navigate to main tabs after successful authentication
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import FastImage from '@d11/react-native-fast-image';
|
|||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { TMDBService } from '../services/tmdbService';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
|
@ -40,6 +41,7 @@ const BackdropGalleryScreen: React.FC = () => {
|
|||
const navigation = useNavigation();
|
||||
const { tmdbId, type, title } = route.params as RouteParams;
|
||||
const { currentTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { settings } = useSettings();
|
||||
|
||||
const [backdrops, setBackdrops] = useState<BackdropItem[]>([]);
|
||||
|
|
@ -75,10 +77,10 @@ const BackdropGalleryScreen: React.FC = () => {
|
|||
if (images && images.backdrops && images.backdrops.length > 0) {
|
||||
setBackdrops(images.backdrops);
|
||||
} else {
|
||||
setError('No backdrops found');
|
||||
setError(t('metadata.no_backdrops'));
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load backdrops');
|
||||
setError(t('metadata.error_backdrops'));
|
||||
console.error('Backdrop fetch error:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
|
|||
|
|
@ -161,8 +161,8 @@ const SpecialMentionCard: React.FC<SpecialMentionCardProps> = ({ mention, curren
|
|||
// Fallback: show alert with Discord info
|
||||
Alert.alert(
|
||||
mention.name,
|
||||
`Discord: @${mention.username}\n\nDo you want to open Discord and search for this user?`,
|
||||
[{ text: 'OK' }]
|
||||
t('mal.discord_open_prompt', { username: mention.username }),
|
||||
[{ text: t('common.ok') }]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -464,7 +464,7 @@ const LibraryScreen = () => {
|
|||
// Sync Trakt watchlist to local library
|
||||
const syncTraktWatchlistToLibrary = useCallback(async () => {
|
||||
if (!traktAuthenticated) {
|
||||
showError('Sync Failed', 'Please connect to Trakt first');
|
||||
showError(t('library.sync_failed'), t('library.sync_connect_first'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -667,11 +667,11 @@ const LibraryScreen = () => {
|
|||
showInfo('Sync Complete', message);
|
||||
logger.log(`[LibraryScreen] Sync complete: ${message}`);
|
||||
} else {
|
||||
showInfo('Sync Complete', 'Library is up to date');
|
||||
showInfo(t('library.sync_complete'), t('library.sync_up_to_date'));
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[LibraryScreen] Sync failed:', error);
|
||||
showError('Sync Failed', 'Unable to sync. Please try again.');
|
||||
showError(t('library.sync_failed'), t('library.sync_error'));
|
||||
} finally {
|
||||
setIsSyncing(false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,24 +98,24 @@ const MalSettingsScreen: React.FC = () => {
|
|||
const result = await MalAuth.login();
|
||||
if (result === true) {
|
||||
await checkAuthStatus();
|
||||
openAlert('Success', 'Connected to MyAnimeList');
|
||||
openAlert(t('common.success'), t('mal.connected'));
|
||||
} else {
|
||||
const errorMessage = typeof result === 'string' ? result : 'Failed to connect to MyAnimeList';
|
||||
openAlert('Error', errorMessage);
|
||||
const errorMessage = typeof result === 'string' ? result : t('mal.error_connect');
|
||||
openAlert(t('common.error'), errorMessage);
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
openAlert('Error', `An error occurred during sign in: ${e.message || 'Unknown error'}`);
|
||||
openAlert(t('common.error'), t('mal.error_sign_in', { message: e.message || t('common.unknown') }));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignOut = () => {
|
||||
openAlert('Sign Out', 'Are you sure you want to disconnect?', [
|
||||
{ label: 'Cancel', onPress: () => setAlertVisible(false) },
|
||||
{
|
||||
label: 'Sign Out',
|
||||
openAlert(t('mal.sign_out'), t('mal.sign_out_confirm'), [
|
||||
{ label: t('common.cancel'), onPress: () => setAlertVisible(false) },
|
||||
{
|
||||
label: t('mal.sign_out'),
|
||||
onPress: () => {
|
||||
MalAuth.clearToken();
|
||||
setIsAuthenticated(false);
|
||||
|
|
@ -168,13 +168,13 @@ const MalSettingsScreen: React.FC = () => {
|
|||
color={currentTheme.colors.highEmphasis}
|
||||
/>
|
||||
<Text style={[styles.backText, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Settings
|
||||
{t('common.settings')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||
MyAnimeList
|
||||
{t('mal.title')}
|
||||
</Text>
|
||||
|
||||
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent}>
|
||||
|
|
@ -232,47 +232,47 @@ const MalSettingsScreen: React.FC = () => {
|
|||
<Text style={[styles.statValue, { color: currentTheme.colors.primary }]}>
|
||||
{userProfile.anime_statistics.num_items}
|
||||
</Text>
|
||||
<Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>Total</Text>
|
||||
<Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('mal.stat_total')}</Text>
|
||||
</View>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={[styles.statValue, { color: currentTheme.colors.primary }]}>
|
||||
{userProfile.anime_statistics.num_days_watched.toFixed(1)}
|
||||
</Text>
|
||||
<Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>Days</Text>
|
||||
<Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('mal.stat_days')}</Text>
|
||||
</View>
|
||||
<View style={styles.statBox}>
|
||||
<Text style={[styles.statValue, { color: currentTheme.colors.primary }]}>
|
||||
{userProfile.anime_statistics.mean_score.toFixed(1)}
|
||||
</Text>
|
||||
<Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>Mean</Text>
|
||||
<Text style={[styles.statLabel, { color: currentTheme.colors.mediumEmphasis }]}>{t('mal.stat_mean')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={[styles.statGrid, { borderColor: currentTheme.colors.border }]}>
|
||||
<View style={styles.statGridItem}>
|
||||
<View style={[styles.statusDot, { backgroundColor: '#2DB039' }]} />
|
||||
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>Watching</Text>
|
||||
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>{t('mal.watching')}</Text>
|
||||
<Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}>
|
||||
{userProfile.anime_statistics.num_items_watching}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.statGridItem}>
|
||||
<View style={[styles.statusDot, { backgroundColor: '#26448F' }]} />
|
||||
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>Completed</Text>
|
||||
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>{t('mal.completed')}</Text>
|
||||
<Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}>
|
||||
{userProfile.anime_statistics.num_items_completed}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.statGridItem}>
|
||||
<View style={[styles.statusDot, { backgroundColor: '#F9D457' }]} />
|
||||
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>On Hold</Text>
|
||||
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>{t('mal.on_hold')}</Text>
|
||||
<Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}>
|
||||
{userProfile.anime_statistics.num_items_on_hold}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.statGridItem}>
|
||||
<View style={[styles.statusDot, { backgroundColor: '#A12F31' }]} />
|
||||
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>Dropped</Text>
|
||||
<Text style={[styles.statGridLabel, { color: currentTheme.colors.highEmphasis }]}>{t('mal.dropped')}</Text>
|
||||
<Text style={[styles.statGridValue, { color: currentTheme.colors.highEmphasis }]}>
|
||||
{userProfile.anime_statistics.num_items_dropped}
|
||||
</Text>
|
||||
|
|
@ -289,26 +289,26 @@ const MalSettingsScreen: React.FC = () => {
|
|||
try {
|
||||
const synced = await MalSync.syncMalToLibrary();
|
||||
if (synced) {
|
||||
openAlert('Sync Complete', 'MAL data has been refreshed.');
|
||||
openAlert(t('mal.sync_complete'), t('mal.sync_complete_msg'));
|
||||
} else {
|
||||
openAlert('Sync Failed', 'Could not refresh MAL data.');
|
||||
openAlert(t('mal.sync_failed'), t('mal.sync_failed_msg'));
|
||||
}
|
||||
} catch {
|
||||
openAlert('Sync Failed', 'Could not refresh MAL data.');
|
||||
openAlert(t('mal.sync_failed'), t('mal.sync_failed_msg'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="sync" size={18} color="white" style={{ marginRight: 6 }} />
|
||||
<Text style={styles.buttonText}>Sync</Text>
|
||||
<Text style={styles.buttonText}>{t('mal.sync_button')}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.smallButton, { backgroundColor: currentTheme.colors.error, width: 100 }]}
|
||||
onPress={handleSignOut}
|
||||
>
|
||||
<Text style={styles.buttonText}>Sign Out</Text>
|
||||
<Text style={styles.buttonText}>{t('mal.sign_out')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
|
@ -320,16 +320,16 @@ const MalSettingsScreen: React.FC = () => {
|
|||
resizeMode="contain"
|
||||
/>
|
||||
<Text style={[styles.signInTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Connect MyAnimeList
|
||||
{t('mal.connect_title')}
|
||||
</Text>
|
||||
<Text style={[styles.signInDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Sync your watch history and manage your anime list.
|
||||
{t('mal.connect_desc')}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
style={[styles.button, { backgroundColor: currentTheme.colors.primary }]}
|
||||
onPress={handleSignIn}
|
||||
>
|
||||
<Text style={styles.buttonText}>Sign In with MAL</Text>
|
||||
<Text style={styles.buttonText}>{t('mal.sign_in')}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
|
@ -339,17 +339,17 @@ const MalSettingsScreen: React.FC = () => {
|
|||
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
||||
<View style={styles.settingsSection}>
|
||||
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Sync Settings
|
||||
{t('mal.sync_settings')}
|
||||
</Text>
|
||||
|
||||
<View style={styles.settingItem}>
|
||||
<View style={styles.settingContent}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Enable MAL Sync
|
||||
{t('mal.enable_sync')}
|
||||
</Text>
|
||||
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Global switch to enable or disable all MyAnimeList features.
|
||||
{t('mal.enable_sync_desc')}
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
|
|
@ -365,10 +365,10 @@ const MalSettingsScreen: React.FC = () => {
|
|||
<View style={styles.settingContent}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Auto Episode Update
|
||||
{t('mal.auto_episode')}
|
||||
</Text>
|
||||
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Automatically update your progress on MAL when you finish watching an episode (>=90% completion).
|
||||
{t('mal.auto_episode_desc')}
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
|
|
@ -384,10 +384,10 @@ const MalSettingsScreen: React.FC = () => {
|
|||
<View style={styles.settingContent}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Auto Add Anime
|
||||
{t('mal.auto_add')}
|
||||
</Text>
|
||||
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
If an anime is not in your MAL list, it will be added automatically when you start watching.
|
||||
{t('mal.auto_add_desc')}
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
|
|
@ -403,10 +403,10 @@ const MalSettingsScreen: React.FC = () => {
|
|||
<View style={styles.settingContent}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Auto-Sync to Library
|
||||
{t('mal.auto_sync_library')}
|
||||
</Text>
|
||||
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Automatically add items from your MAL 'Watching' list to your Nuvio Library.
|
||||
{t('mal.auto_sync_library_desc')}
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
|
|
@ -422,10 +422,10 @@ const MalSettingsScreen: React.FC = () => {
|
|||
<View style={styles.settingContent}>
|
||||
<View style={styles.settingTextContainer}>
|
||||
<Text style={[styles.settingLabel, { color: currentTheme.colors.highEmphasis }]}>
|
||||
Include NSFW Content
|
||||
{t('mal.include_nsfw')}
|
||||
</Text>
|
||||
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis }]}>
|
||||
Allow NSFW entries to be returned when fetching your MAL list.
|
||||
{t('mal.include_nsfw_desc')}
|
||||
</Text>
|
||||
</View>
|
||||
<Switch
|
||||
|
|
|
|||
|
|
@ -917,16 +917,16 @@ const PluginsScreen: React.FC = () => {
|
|||
// Small delay to ensure UI is ready
|
||||
setTimeout(() => {
|
||||
openAlert(
|
||||
'Add Repository',
|
||||
`Do you want to add the repository from:\n${url}`,
|
||||
t('plugins.add_repo'),
|
||||
t('plugins.add_repo_confirm', { url }),
|
||||
[
|
||||
{
|
||||
label: 'Cancel',
|
||||
label: t('common.cancel'),
|
||||
onPress: () => { },
|
||||
style: { color: colors.error }
|
||||
},
|
||||
{
|
||||
label: 'Add',
|
||||
label: t('plugins.add'),
|
||||
onPress: () => {
|
||||
handleAddRepository(url);
|
||||
}
|
||||
|
|
@ -950,7 +950,7 @@ const PluginsScreen: React.FC = () => {
|
|||
) => {
|
||||
setAlertTitle(title);
|
||||
setAlertMessage(message);
|
||||
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]);
|
||||
setAlertActions(actions && actions.length > 0 ? actions : [{ label: t('common.ok'), onPress: () => { } }]);
|
||||
setAlertVisible(true);
|
||||
};
|
||||
|
||||
|
|
@ -1060,7 +1060,7 @@ const PluginsScreen: React.FC = () => {
|
|||
openAlert(t('plugins.success'), `${enabled ? t('plugins.enabled') : t('plugins.disabled')} ${filteredPlugins.length} extensions`);
|
||||
} catch (error) {
|
||||
logger.error('[PluginSettings] Failed to bulk toggle:', error);
|
||||
openAlert(t('plugins.error'), 'Failed to update extensions');
|
||||
openAlert(t('plugins.error'), t('plugins.error_update_extensions'));
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
|
|
@ -1076,7 +1076,7 @@ const PluginsScreen: React.FC = () => {
|
|||
const inputUrl = validUrlOverride || newRepositoryUrl;
|
||||
|
||||
if (!inputUrl.trim()) {
|
||||
openAlert('Error', 'Please enter a valid repository URL');
|
||||
openAlert(t('common.error'), t('plugins.error_valid_url'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1085,7 +1085,7 @@ const PluginsScreen: React.FC = () => {
|
|||
if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) {
|
||||
openAlert(
|
||||
t('plugins.alert_invalid_url'),
|
||||
'Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch\n\nor include manifest.json:\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch/manifest.json\n\nExample:\nhttps://raw.githubusercontent.com/your-username/your-repo/refs/heads/main'
|
||||
t('plugins.error_valid_url_format')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1166,7 +1166,7 @@ const PluginsScreen: React.FC = () => {
|
|||
openAlert(t('plugins.success'), `Repository "${repo?.name || t('plugins.unknown')}" ${enabled ? t('plugins.enabled').toLowerCase() : t('plugins.disabled').toLowerCase()} successfully`);
|
||||
} catch (error) {
|
||||
logger.error('[PluginSettings] Failed to toggle repository:', error);
|
||||
openAlert(t('plugins.error'), 'Failed to update repository');
|
||||
openAlert(t('plugins.error'), t('plugins.error_update_repo'));
|
||||
} finally {
|
||||
setSwitchingRepository(null);
|
||||
}
|
||||
|
|
@ -1179,30 +1179,30 @@ const PluginsScreen: React.FC = () => {
|
|||
// Special handling for the last repository
|
||||
const isLastRepository = repositories.length === 1;
|
||||
|
||||
const alertTitle = isLastRepository ? 'Remove Last Repository' : 'Remove Repository';
|
||||
const alertTitleText = isLastRepository ? t('plugins.remove_last_repo') : t('plugins.remove_repo');
|
||||
const alertMessage = isLastRepository
|
||||
? `Are you sure you want to remove "${repo.name}"? This is your only repository, so you'll have no extensions available until you add a new repository.`
|
||||
: `Are you sure you want to remove "${repo.name}"? This will also remove all extensions from this repository.`;
|
||||
? t('plugins.remove_last_repo_desc', { name: repo.name })
|
||||
: t('plugins.remove_repo_desc', { name: repo.name });
|
||||
|
||||
openAlert(
|
||||
alertTitle,
|
||||
alertTitleText,
|
||||
alertMessage,
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{ label: t('common.cancel'), onPress: () => { } },
|
||||
{
|
||||
label: 'Remove',
|
||||
label: t('plugins.remove'),
|
||||
onPress: async () => {
|
||||
try {
|
||||
await pluginService.removeRepository(repoId);
|
||||
await loadRepositories();
|
||||
await loadPlugins();
|
||||
const successMessage = isLastRepository
|
||||
? 'Repository removed successfully. You can add a new repository using the "Add Repository" button.'
|
||||
: 'Repository removed successfully';
|
||||
openAlert('Success', successMessage);
|
||||
? t('plugins.remove_last_repo_success')
|
||||
: t('plugins.remove_repo_success');
|
||||
openAlert(t('common.success'), successMessage);
|
||||
} catch (error) {
|
||||
logger.error('[PluginSettings] Failed to remove repository:', error);
|
||||
openAlert('Error', error instanceof Error ? error.message : 'Failed to remove repository');
|
||||
openAlert(t('common.error'), error instanceof Error ? error.message : t('plugins.error_remove_repo'));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -1282,7 +1282,7 @@ const PluginsScreen: React.FC = () => {
|
|||
|
||||
const handleSaveRepository = async () => {
|
||||
if (!repositoryUrl.trim()) {
|
||||
openAlert('Error', 'Please enter a valid repository URL');
|
||||
openAlert(t('common.error'), t('plugins.error_valid_url'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1290,8 +1290,8 @@ const PluginsScreen: React.FC = () => {
|
|||
const url = repositoryUrl.trim();
|
||||
if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) {
|
||||
openAlert(
|
||||
'Invalid URL Format',
|
||||
'Please use a valid GitHub raw URL format:\n\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch\n\nExample:\nhttps://raw.githubusercontent.com/your-username/your-repo/refs/heads/main'
|
||||
t('plugins.alert_invalid_url'),
|
||||
t('plugins.error_valid_url_format')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -1304,7 +1304,7 @@ const PluginsScreen: React.FC = () => {
|
|||
openAlert(t('plugins.success'), t('plugins.alert_repo_saved'));
|
||||
} catch (error) {
|
||||
logger.error('[PluginSettings] Failed to save repository:', error);
|
||||
openAlert(t('plugins.error'), 'Failed to save repository URL');
|
||||
openAlert(t('plugins.error'), t('plugins.error_save_repo'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -1312,7 +1312,7 @@ const PluginsScreen: React.FC = () => {
|
|||
|
||||
const handleRefreshRepository = async () => {
|
||||
if (!repositoryUrl.trim()) {
|
||||
openAlert('Error', 'Please set a repository URL first');
|
||||
openAlert(t('common.error'), t('plugins.error_set_repo_first'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -1331,8 +1331,8 @@ const PluginsScreen: React.FC = () => {
|
|||
logger.error('[PluginsScreen] Failed to refresh repository:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
openAlert(
|
||||
'Repository Error',
|
||||
`Failed to refresh repository: ${errorMessage}\n\nPlease ensure your URL is correct and follows this format:\nhttps://raw.githubusercontent.com/username/repo/refs/heads/branch`
|
||||
t('plugins.error_repo'),
|
||||
t('plugins.error_refresh_repo', { error: errorMessage })
|
||||
);
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
|
|
@ -1358,7 +1358,7 @@ const PluginsScreen: React.FC = () => {
|
|||
await loadPlugins();
|
||||
} catch (error) {
|
||||
logger.error('[PluginSettings] Failed to toggle plugin:', error);
|
||||
openAlert(t('plugins.error'), 'Failed to update extension status');
|
||||
openAlert(t('plugins.error'), t('plugins.error_update_status'));
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
|
@ -1368,9 +1368,9 @@ const PluginsScreen: React.FC = () => {
|
|||
t('plugins.clear_all'),
|
||||
t('plugins.clear_all_desc'),
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{ label: t('common.cancel'), onPress: () => { } },
|
||||
{
|
||||
label: 'Clear',
|
||||
label: t('plugins.clear'),
|
||||
onPress: async () => {
|
||||
try {
|
||||
await pluginService.clearScrapers();
|
||||
|
|
@ -1378,7 +1378,7 @@ const PluginsScreen: React.FC = () => {
|
|||
openAlert(t('plugins.success'), t('plugins.alert_plugins_cleared'));
|
||||
} catch (error) {
|
||||
logger.error('[PluginSettings] Failed to clear plugins:', error);
|
||||
openAlert(t('plugins.error'), 'Failed to clear extensions');
|
||||
openAlert(t('plugins.error'), t('plugins.error_clear_extensions'));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -1391,9 +1391,9 @@ const PluginsScreen: React.FC = () => {
|
|||
t('plugins.clear_cache'),
|
||||
t('plugins.clear_cache_desc'),
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{ label: t('common.cancel'), onPress: () => { } },
|
||||
{
|
||||
label: 'Clear Cache',
|
||||
label: t('plugins.clear_cache'),
|
||||
onPress: async () => {
|
||||
try {
|
||||
await pluginService.clearScrapers();
|
||||
|
|
@ -1405,7 +1405,7 @@ const PluginsScreen: React.FC = () => {
|
|||
openAlert(t('plugins.success'), t('plugins.alert_cache_cleared'));
|
||||
} catch (error) {
|
||||
logger.error('[PluginSettings] Failed to clear cache:', error);
|
||||
openAlert(t('plugins.error'), 'Failed to clear repository cache');
|
||||
openAlert(t('plugins.error'), t('plugins.error_clear_cache'));
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
@ -1927,7 +1927,7 @@ const PluginsScreen: React.FC = () => {
|
|||
});
|
||||
}
|
||||
setShowboxSavedToken(showboxUiToken);
|
||||
openAlert('Saved', 'ShowBox settings updated');
|
||||
openAlert(t('plugins.showbox_saved'), t('plugins.showbox_saved_msg'));
|
||||
}}
|
||||
>
|
||||
<Text style={styles.buttonText}>{t('plugins.save')}</Text>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTraktContext } from '../contexts/TraktContext';
|
||||
import { mmkvStorage } from '../services/mmkvStorage';
|
||||
import CustomAlert from '../components/CustomAlert';
|
||||
|
|
@ -33,6 +34,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
const navigation = useNavigation();
|
||||
const { currentTheme } = useTheme();
|
||||
const { isAuthenticated, userProfile, refreshAuthStatus } = useTraktContext();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [profiles, setProfiles] = useState<Profile[]>([]);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
|
|
@ -76,7 +78,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
}
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('Error loading profiles:', error);
|
||||
openAlert('Error', 'Failed to load profiles');
|
||||
openAlert(t('common.error'), t('profiles.error_load'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
|
@ -102,7 +104,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
await mmkvStorage.setItem(PROFILE_STORAGE_KEY, JSON.stringify(updatedProfiles));
|
||||
} catch (error) {
|
||||
if (__DEV__) console.error('Error saving profiles:', error);
|
||||
openAlert('Error', 'Failed to save profiles');
|
||||
openAlert(t('common.error'), t('profiles.error_save'));
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
|
@ -118,7 +120,7 @@ const ProfilesScreen: React.FC = () => {
|
|||
|
||||
const handleAddProfile = useCallback(() => {
|
||||
if (!newProfileName.trim()) {
|
||||
openAlert('Error', 'Please enter a profile name');
|
||||
openAlert(t('common.error'), t('profiles.error_empty_name'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -150,23 +152,23 @@ const ProfilesScreen: React.FC = () => {
|
|||
// Prevent deleting the active profile
|
||||
const isActiveProfile = profiles.find(p => p.id === id)?.isActive;
|
||||
if (isActiveProfile) {
|
||||
openAlert('Error', 'Cannot delete the active profile. Switch to another profile first.');
|
||||
openAlert(t('common.error'), t('profiles.error_delete_active'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent deleting the last profile
|
||||
if (profiles.length <= 1) {
|
||||
openAlert('Error', 'Cannot delete the only profile');
|
||||
openAlert(t('common.error'), t('profiles.error_delete_only'));
|
||||
return;
|
||||
}
|
||||
|
||||
openAlert(
|
||||
'Delete Profile',
|
||||
'Are you sure you want to delete this profile? This action cannot be undone.',
|
||||
t('profiles.delete_title'),
|
||||
t('profiles.delete_confirm'),
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{ label: t('common.cancel'), onPress: () => { } },
|
||||
{
|
||||
label: 'Delete',
|
||||
label: t('common.delete'),
|
||||
onPress: () => {
|
||||
const updatedProfiles = profiles.filter(profile => profile.id !== id);
|
||||
setProfiles(updatedProfiles);
|
||||
|
|
|
|||
|
|
@ -186,7 +186,7 @@ const SettingsScreen: React.FC = () => {
|
|||
) => {
|
||||
setAlertTitle(title);
|
||||
setAlertMessage(message);
|
||||
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]);
|
||||
setAlertActions(actions && actions.length > 0 ? actions : [{ label: t('common.ok'), onPress: () => { } }]);
|
||||
setAlertVisible(true);
|
||||
};
|
||||
|
||||
|
|
@ -334,18 +334,18 @@ const SettingsScreen: React.FC = () => {
|
|||
|
||||
const handleClearMDBListCache = () => {
|
||||
openAlert(
|
||||
'Clear MDBList Cache',
|
||||
'Are you sure you want to clear all cached MDBList data? This cannot be undone.',
|
||||
t('settings.clear_mdblist_cache_title'),
|
||||
t('settings.clear_mdblist_cache_confirm'),
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{ label: t('common.cancel'), onPress: () => { } },
|
||||
{
|
||||
label: 'Clear',
|
||||
label: t('common.delete'),
|
||||
onPress: async () => {
|
||||
try {
|
||||
await mmkvStorage.removeItem('mdblist_cache');
|
||||
openAlert('Success', 'MDBList cache has been cleared.');
|
||||
openAlert(t('common.success'), t('settings.clear_mdblist_cache_success'));
|
||||
} catch (error) {
|
||||
openAlert('Error', 'Could not clear MDBList cache.');
|
||||
openAlert(t('common.error'), t('settings.clear_mdblist_cache_error'));
|
||||
if (__DEV__) console.error('Error clearing MDBList cache:', error);
|
||||
}
|
||||
}
|
||||
|
|
@ -420,8 +420,8 @@ const SettingsScreen: React.FC = () => {
|
|||
)}
|
||||
{isItemVisible('mal') && (
|
||||
<SettingItem
|
||||
title="MyAnimeList"
|
||||
description="Sync with MyAnimeList"
|
||||
title={t('mal.title')}
|
||||
description={t('mal.description')}
|
||||
customIcon={<Image source={require('../../assets/rating-icons/mal-icon.png')} style={{ width: isTablet ? 24 : 20, height: isTablet ? 24 : 20, borderRadius: 4 }} resizeMode="contain" />}
|
||||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => navigation.navigate('MalSettings')}
|
||||
|
|
@ -438,7 +438,7 @@ const SettingsScreen: React.FC = () => {
|
|||
case 'appearance':
|
||||
return (
|
||||
<>
|
||||
<SettingsCard title="GENERAL" isTablet={isTablet}>
|
||||
<SettingsCard title={t('settings.sections.general')} isTablet={isTablet}>
|
||||
<SettingItem
|
||||
title={t('settings.language')}
|
||||
description={t(`settings.${LOCALES.find(l => l.code === i18n.language)?.key}`)}
|
||||
|
|
@ -476,8 +476,8 @@ const SettingsScreen: React.FC = () => {
|
|||
isTablet={isTablet}
|
||||
/>
|
||||
<SettingItem
|
||||
title={'Plugin Tester'}
|
||||
description={'Run a plugin and inspect logs/streams'}
|
||||
title={t('settings.plugin_tester')}
|
||||
description={t('settings.plugin_tester_desc')}
|
||||
icon="terminal"
|
||||
onPress={() => navigation.navigate('PluginTester')}
|
||||
renderControl={() => <ChevronRight />}
|
||||
|
|
@ -489,9 +489,9 @@ const SettingsScreen: React.FC = () => {
|
|||
onPress={async () => {
|
||||
try {
|
||||
await mmkvStorage.removeItem('hasCompletedOnboarding');
|
||||
openAlert('Success', 'Onboarding has been reset. Restart the app to see the onboarding flow.');
|
||||
openAlert(t('common.success'), t('settings.reset_onboarding_success'));
|
||||
} catch (error) {
|
||||
openAlert('Error', 'Failed to reset onboarding.');
|
||||
openAlert(t('common.error'), t('settings.reset_onboarding_error'));
|
||||
}
|
||||
}}
|
||||
renderControl={() => <ChevronRight />}
|
||||
|
|
@ -503,7 +503,7 @@ const SettingsScreen: React.FC = () => {
|
|||
icon="refresh-cw"
|
||||
onPress={async () => {
|
||||
await campaignService.resetCampaigns();
|
||||
openAlert('Success', 'Campaign history reset. Restart app to see posters again.');
|
||||
openAlert(t('common.success'), t('settings.reset_campaigns_success'));
|
||||
}}
|
||||
renderControl={() => <ChevronRight />}
|
||||
isTablet={isTablet}
|
||||
|
|
@ -516,15 +516,15 @@ const SettingsScreen: React.FC = () => {
|
|||
t('settings.clear_data'),
|
||||
t('settings.clear_data_desc'),
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{ label: t('common.cancel'), onPress: () => { } },
|
||||
{
|
||||
label: 'Clear',
|
||||
label: t('common.delete'),
|
||||
onPress: async () => {
|
||||
try {
|
||||
await mmkvStorage.clear();
|
||||
openAlert('Success', 'All data cleared. Please restart the app.');
|
||||
openAlert(t('common.success'), t('settings.clear_data_success'));
|
||||
} catch (error) {
|
||||
openAlert('Error', 'Failed to clear data.');
|
||||
openAlert(t('common.error'), t('settings.clear_data_error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -717,8 +717,8 @@ const SettingsScreen: React.FC = () => {
|
|||
<SettingsCard title={t('settings.account').toUpperCase()}>
|
||||
{showCloudSyncItem && (
|
||||
<SettingItem
|
||||
title="Nuvio Sync"
|
||||
description="Sync data across your Nuvio devices"
|
||||
title={t('settings.cloud_sync.title')}
|
||||
description={t('settings.cloud_sync.description')}
|
||||
customIcon={
|
||||
<FastImage
|
||||
source={require('../../assets/nuvio-sync-icon-og.png')}
|
||||
|
|
@ -753,8 +753,8 @@ const SettingsScreen: React.FC = () => {
|
|||
)}
|
||||
{isItemVisible('mal') && (
|
||||
<SettingItem
|
||||
title="MyAnimeList"
|
||||
description="Sync with MyAnimeList"
|
||||
title={t('mal.title')}
|
||||
description={t('mal.description')}
|
||||
customIcon={<Image source={require('../../assets/rating-icons/mal-icon.png')} style={{ width: 20, height: 20, borderRadius: 4 }} resizeMode="contain" />}
|
||||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => navigation.navigate('MalSettings')}
|
||||
|
|
@ -771,7 +771,7 @@ const SettingsScreen: React.FC = () => {
|
|||
(settingsConfig?.categories?.['integrations']?.visible !== false) ||
|
||||
(settingsConfig?.categories?.['playback']?.visible !== false)
|
||||
) && (
|
||||
<SettingsCard title="GENERAL">
|
||||
<SettingsCard title={t('settings.sections.general')}>
|
||||
<SettingItem
|
||||
title={t('settings.language')}
|
||||
description={t(`settings.${LOCALES.find(l => l.code === i18n.language)?.key}`)
|
||||
|
|
@ -825,11 +825,11 @@ const SettingsScreen: React.FC = () => {
|
|||
(settingsConfig?.categories?.['backup']?.visible !== false) ||
|
||||
(settingsConfig?.categories?.['updates']?.visible !== false)
|
||||
) && (
|
||||
<SettingsCard title="DATA">
|
||||
<SettingsCard title={t('settings.sections.data')}>
|
||||
{(settingsConfig?.categories?.['backup']?.visible !== false) && (
|
||||
<SettingItem
|
||||
title={t('settings.backup_restore')}
|
||||
description="Create and restore app backups"
|
||||
description={t('settings.backup_restore_desc')}
|
||||
icon="archive"
|
||||
renderControl={() => <ChevronRight />}
|
||||
onPress={() => navigation.navigate('Backup')}
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
|
||||
const handleSignIn = () => {
|
||||
if (isSimklAuthenticated) {
|
||||
openAlert('Conflict', 'You cannot connect to Trakt while Simkl is connected. Please disconnect Simkl first.');
|
||||
openAlert(t('trakt.conflict_title'), t('trakt.conflict_simkl_msg'));
|
||||
return;
|
||||
}
|
||||
promptAsync(); // Trigger the authentication flow
|
||||
|
|
@ -289,18 +289,18 @@ const TraktSettingsScreen: React.FC = () => {
|
|||
// Show confirmation
|
||||
const modeLabel = LIBRARY_SYNC_MODE_OPTIONS.find(o => o.value === mode)?.label || mode;
|
||||
openAlert(
|
||||
'Library Sync Mode Updated',
|
||||
`Trakt library sync is now set to: ${modeLabel}`
|
||||
t('trakt.library_sync_updated'),
|
||||
t('trakt.library_sync_updated_msg', { mode: modeLabel })
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error('[TraktSettingsScreen] Failed to save library sync mode:', error);
|
||||
openAlert('Error', 'Failed to update library sync mode');
|
||||
openAlert(t('common.error'), t('trakt.library_sync_error'));
|
||||
}
|
||||
};
|
||||
|
||||
const getLibrarySyncModeLabel = (mode: string): string => {
|
||||
const option = LIBRARY_SYNC_MODE_OPTIONS.find(o => o.value === mode);
|
||||
return option?.label || 'Off';
|
||||
return option?.label || t('common.disable');
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -44,38 +44,38 @@ const DeveloperSettingsScreen: React.FC = () => {
|
|||
) => {
|
||||
setAlertTitle(title);
|
||||
setAlertMessage(message);
|
||||
setAlertActions(actions && actions.length > 0 ? actions : [{ label: 'OK', onPress: () => { } }]);
|
||||
setAlertActions(actions && actions.length > 0 ? actions : [{ label: t('common.ok'), onPress: () => { } }]);
|
||||
setAlertVisible(true);
|
||||
};
|
||||
|
||||
const handleResetOnboarding = async () => {
|
||||
try {
|
||||
await mmkvStorage.removeItem('hasCompletedOnboarding');
|
||||
openAlert('Success', 'Onboarding has been reset. Restart the app to see the onboarding flow.');
|
||||
openAlert(t('common.success'), t('settings.reset_onboarding_success'));
|
||||
} catch (error) {
|
||||
openAlert('Error', 'Failed to reset onboarding.');
|
||||
openAlert(t('common.error'), t('settings.reset_onboarding_error'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleResetCampaigns = async () => {
|
||||
await campaignService.resetCampaigns();
|
||||
openAlert('Success', 'Campaign history reset. Restart app to see posters again.');
|
||||
openAlert(t('common.success'), t('settings.reset_campaigns_success'));
|
||||
};
|
||||
|
||||
const handleClearAllData = () => {
|
||||
openAlert(
|
||||
'Clear All Data',
|
||||
'This will reset all settings and clear all cached data. Are you sure?',
|
||||
t('settings.items.clear_all_data'),
|
||||
t('settings.clear_data_desc'),
|
||||
[
|
||||
{ label: 'Cancel', onPress: () => { } },
|
||||
{ label: t('common.cancel'), onPress: () => { } },
|
||||
{
|
||||
label: 'Clear',
|
||||
label: t('common.delete'),
|
||||
onPress: async () => {
|
||||
try {
|
||||
await mmkvStorage.clear();
|
||||
openAlert('Success', 'All data cleared. Please restart the app.');
|
||||
openAlert(t('common.success'), t('settings.clear_data_success'));
|
||||
} catch (error) {
|
||||
openAlert('Error', 'Failed to clear data.');
|
||||
openAlert(t('common.error'), t('settings.clear_data_error'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useMetadataAssets } from '../../hooks/useMetadataAssets';
|
|||
import { useSettings } from '../../hooks/useSettings';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { useTrailer } from '../../contexts/TrailerContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToast } from '../../contexts/ToastContext';
|
||||
import { useDominantColor } from '../../hooks/useDominantColor';
|
||||
import { Stream } from '../../types/metadata';
|
||||
|
|
@ -49,6 +50,7 @@ export const useStreamsScreen = () => {
|
|||
const { currentTheme } = useTheme();
|
||||
const { colors } = currentTheme;
|
||||
const { pauseTrailer, resumeTrailer } = useTrailer();
|
||||
const { t } = useTranslation();
|
||||
const { showSuccess, showInfo } = useToast();
|
||||
|
||||
// Dimension tracking
|
||||
|
|
@ -465,7 +467,7 @@ export const useStreamsScreen = () => {
|
|||
|
||||
// Block magnet links
|
||||
if (typeof stream.url === 'string' && stream.url.startsWith('magnet:')) {
|
||||
openAlert('Not supported', 'Torrent streaming is not supported yet.');
|
||||
openAlert(t('streams.torrent_not_supported'), t('streams.torrent_not_supported_msg'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue