updated sub pages

This commit is contained in:
tapframe 2026-01-06 12:07:37 +05:30
parent afddf4bf2d
commit ed4950cd1f
14 changed files with 1326 additions and 614 deletions

View file

@ -139,6 +139,333 @@
"external_first_desc": "Prefer addon subtitles, then embedded",
"any_available": "Any Available",
"any_available_desc": "Use first available subtitle track"
}
},
"trakt": "Trakt",
"clear_data": "Clear All Data",
"clear_data_desc": "This will reset all settings and clear all cached data. Are you sure?",
"app_updates": "App Updates",
"about_nuvio": "About Nuvio"
},
"ai_settings": {
"title": "AI Assistant",
"info_title": "AI-Powered Chat",
"info_desc": "Ask questions about any movie or TV show episode using advanced AI. Get insights about plot, characters, themes, trivia, and more - all powered by comprehensive TMDB data.",
"feature_1": "Episode-specific context and analysis",
"feature_2": "Plot explanations and character insights",
"feature_3": "Behind-the-scenes trivia and facts",
"feature_4": "Your own free OpenRouter API key",
"api_key_section": "OPENROUTER API KEY",
"api_key_label": "API Key",
"api_key_desc": "Enter your OpenRouter API key to enable AI chat features",
"save_api_key": "Save API Key",
"saving": "Saving...",
"update": "Update",
"remove": "Remove",
"get_free_key": "Get Free API Key from OpenRouter",
"enable_chat": "Enable AI Chat",
"enable_chat_desc": "When enabled, the Ask AI button will appear on content pages.",
"chat_enabled": "AI Chat Enabled",
"chat_enabled_desc": "You can now ask questions about movies and TV shows. Look for the \"Ask AI\" button on content pages!",
"how_it_works": "How it works",
"how_it_works_desc": "• OpenRouter provides access to multiple AI models\n• Your API key stays private and secure\n• Free tier includes generous usage limits\n• Chat with context about specific episodes/movies\n• Get detailed analysis and explanations",
"error_invalid_key": "Please enter a valid API key",
"error_key_format": "OpenRouter API keys should start with \"sk-or-\"",
"success_saved": "OpenRouter API key saved successfully!",
"error_save": "Failed to save API key",
"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"
},
"catalog_settings": {
"title": "Catalogs",
"layout_phone": "LAYOUT CATALOGSCREEN (PHONE)",
"posters_per_row": "Posters per row",
"auto": "Auto",
"show_titles": "Show Poster Titles",
"show_titles_desc": "Display title text below each poster",
"phone_only_hint": "Applies to phones only. Tablets keep adaptive layout.",
"catalogs_group": "Catalogs",
"enabled_count": "{{enabled}} of {{total}} enabled",
"rename_hint": "Long-press a catalog to rename",
"rename_modal_title": "Rename Catalog",
"rename_placeholder": "Enter new catalog name",
"error_save_name": "Could not save the custom name."
},
"continue_watching_settings": {
"title": "Continue Watching",
"playback_behavior": "PLAYBACK BEHAVIOR",
"use_cached": "Use Cached Streams",
"use_cached_desc": "When enabled, clicking Continue Watching items will open the player directly using previously played streams. When disabled, opens a content screen instead.",
"open_metadata": "Open Metadata Screen",
"open_metadata_desc": "When cached streams are disabled, open the Metadata screen instead of the Streams screen. This shows content details and allows manual stream selection.",
"card_appearance": "CARD APPEARANCE",
"card_style": "Card Style",
"card_style_desc": "Choose how Continue Watching items appear on the home screen",
"wide": "Wide",
"poster": "Poster",
"cache_settings": "CACHE SETTINGS",
"cache_duration": "Stream Cache Duration",
"cache_duration_desc": "How long to keep cached stream links before they expire",
"important_note": "Important Note",
"important_note_text": "Not all stream links may remain active for the full cache duration. Longer cache times may result in expired links. If a cached link fails, the app will fall back to fetching fresh streams.",
"how_it_works": "How it works",
"how_it_works_cached": "• Streams are cached for your selected duration after playing\n• Cached streams are validated before use\n• If cache is invalid or expired, falls back to content screen\n• \"Use Cached Streams\" controls direct player vs screen navigation\n• \"Open Metadata Screen\" appears only when cached streams are disabled",
"how_it_works_uncached": "• When cached streams are disabled, clicking Continue Watching items opens content screens\n• \"Open Metadata Screen\" option controls which screen to open\n• Metadata screen shows content details and allows manual stream selection\n• Streams screen shows available streams for immediate playback",
"changes_saved": "Changes saved",
"min": "min",
"hour": "hour",
"hours": "hours"
},
"contributors": {
"title": "Contributors",
"special_mentions": "Special Mentions",
"tab_contributors": "Contributors",
"tab_special": "Special Mentions",
"manager_role": "Community Manager",
"manager_desc": "Manages the Discord & Reddit communities for Nuvio",
"sponsor_role": "Server Sponsor",
"sponsor_desc": "Sponsored the server infrastructure for Nuvio",
"mod_role": "Discord Mod",
"mod_desc": "Helps moderate the Nuvio Discord community",
"loading": "Loading...",
"discord_user": "Discord User",
"contributions": "contributions",
"gratitude_title": "We're grateful for every contribution",
"gratitude_desc": "Each line of code, bug report, and suggestion helps make Nuvio better for everyone",
"special_thanks_title": "Special Thanks",
"special_thanks_desc": "These amazing people help keep the Nuvio community running and the servers online",
"error_rate_limit": "GitHub API rate limit exceeded. Please try again later or pull to refresh.",
"error_failed": "Failed to load contributors. Please check your internet connection.",
"retry": "Try Again",
"no_contributors": "No contributors found",
"loading_contributors": "Loading contributors..."
},
"debrid": {
"title": "Debrid Integration",
"description_torbox": "Unlock 4K high-quality streams and lightning-fast speeds by integrating Torbox. Enter your API Key below to instantly upgrade your streaming experience.",
"description_torrentio": "Configure Torrentio to get torrent streams for movies and TV shows. A debrid service is required to stream content.",
"tab_torbox": "TorBox",
"tab_torrentio": "Torrentio",
"status_connected": "Connected",
"status_disconnected": "Disconnected",
"enable_addon": "Enable Addon",
"disconnect_button": "Disconnect & Remove",
"disconnect_loading": "Disconnecting...",
"account_info": "Account Information",
"plan": "Plan",
"plan_free": "Free",
"plan_essential": "Essential ($3/mo)",
"plan_pro": "Pro ($10/mo)",
"plan_standard": "Standard ($5/mo)",
"plan_unknown": "Unknown",
"expires": "Expires",
"downloaded": "Downloaded",
"status_active": "Active",
"connected_title": "✓ Connected to TorBox",
"connected_desc": "Your TorBox addon is active and providing premium streams.",
"configure_title": "Configure Addon",
"configure_desc": "Customize your streaming experience. Sort by quality, filter file sizes, and manage other integration settings.",
"open_settings": "Open Settings",
"what_is_debrid": "What is a Debrid Service?",
"enter_api_key": "Enter your API Key",
"connect_button": "Connect & Install",
"connecting": "Connecting...",
"unlock_speeds_title": "Unlock Premium Speeds",
"unlock_speeds_desc": "Get a Torbox subscription to access cached high-quality streams with zero buffering.",
"get_subscription": "Get Subscription",
"powered_by": "Powered by",
"disclaimer_torbox": "Nuvio is not affiliated with Torbox in any way.",
"disclaimer_torrentio": "Nuvio is not affiliated with Torrentio in any way.",
"installed_badge": "✓ INSTALLED",
"promo_title": "⚡ Need a Debrid Service?",
"promo_desc": "Get TorBox for lightning-fast 4K streaming with zero buffering. Premium cached torrents and instant downloads.",
"promo_button": "Get TorBox Subscription",
"service_label": "Debrid Service *",
"api_key_label": "API Key *",
"sorting_label": "Sorting",
"exclude_qualities": "Exclude Qualities",
"priority_languages": "Priority Languages",
"max_results": "Max Results",
"additional_options": "Additional Options",
"no_download_links": "Don't show download links",
"no_debrid_catalog": "Don't show debrid catalog",
"install_button": "Install Torrentio",
"installing": "Installing...",
"update_button": "Update Configuration",
"updating": "Updating...",
"remove_button": "Remove Torrentio",
"error_api_required": "API Key Required",
"error_api_required_desc": "Please enter your debrid service API key to install Torrentio.",
"success_installed": "Torrentio addon installed successfully!",
"success_removed": "Torrentio addon removed successfully",
"alert_disconnect_title": "Disconnect Torbox",
"alert_disconnect_msg": "Are you sure you want to disconnect Torbox? This will remove the addon and clear your saved API key."
},
"home_screen": {
"title": "Home Screen Settings",
"changes_applied": "Changes Applied",
"display_options": "DISPLAY OPTIONS",
"show_hero": "Show Hero Section",
"show_hero_desc": "Featured content at the top",
"show_this_week": "Show This Week Section",
"show_this_week_desc": "New episodes from current week",
"select_catalogs": "Select Catalogs",
"all_catalogs": "All catalogs",
"selected": "selected",
"hero_layout": "Hero Layout",
"layout_legacy": "Legacy",
"layout_carousel": "Carousel",
"layout_appletv": "Apple TV",
"layout_desc": "Full-width banner, swipeable cards, or Apple TV style",
"featured_source": "Featured Source",
"using_catalogs": "Using Catalogs",
"manage_catalogs": "Manage selected catalogs",
"dynamic_bg": "Dynamic Hero Background",
"dynamic_bg_desc": "Blurred banner behind carousel",
"performance_note": "May impact performance on low-end devices.",
"posters": "Posters",
"show_titles": "Show Titles",
"poster_size": "Poster Size",
"poster_corners": "Poster Corners",
"size_small": "Small",
"size_medium": "Medium",
"size_large": "Large",
"corner_square": "Square",
"corner_rounded": "Rounded",
"corner_pill": "Pill",
"about_title": "ABOUT THESE SETTINGS",
"about_desc": "These settings control how content is displayed on your Home screen. Changes are applied immediately without requiring an app restart."
},
"mdblist": {
"title": "Rating Sources",
"status_disabled": "MDBList Disabled",
"status_active": "API Key Active",
"status_required": "API Key Required",
"status_disabled_desc": "MDBList functionality is currently disabled.",
"status_active_desc": "Ratings from MDBList are enabled.",
"status_required_desc": "Add your key below to enable ratings.",
"enable_toggle": "Enable MDBList",
"enable_toggle_desc": "Turn on/off all MDBList functionality",
"api_section": "API Key",
"placeholder": "Paste your MDBList API key",
"save": "Save",
"clear": "Clear Key",
"rating_providers": "Rating Providers",
"rating_providers_desc": "Choose which ratings to display in the app",
"how_to": "How to get an API key",
"step_1": "Log in on the",
"step_1_link": "MDBList website",
"step_2": "Go to",
"step_2_settings": "Settings",
"step_2_api": "API",
"step_2_end": "section.",
"step_3": "Generate a new key and copy it.",
"go_to_website": "Go to MDBList",
"alert_clear_title": "Clear API Key",
"alert_clear_msg": "Are you sure you want to remove the saved API key?",
"success_saved": "API key saved successfully.",
"error_empty": "API Key cannot be empty.",
"error_save": "An error occurred while saving. Please try again.",
"api_key_empty_error": "API Key cannot be empty.",
"success_cleared": "API key cleared successfully",
"error_clear": "Failed to clear API key"
},
"notification": {
"title": "Notification Settings",
"section_general": "General",
"enable_notifications": "Enable Notifications",
"section_types": "Notification Types",
"new_episodes": "New Episodes",
"upcoming_shows": "Upcoming Shows",
"reminders": "Reminders",
"section_timing": "Notification Timing",
"timing_desc": "When should you be notified before an episode airs?",
"hours_1": "1 hour",
"hours_suffix": "hours",
"section_status": "Notification Status",
"stats_upcoming": "Upcoming",
"stats_this_week": "This Week",
"stats_total": "Total",
"sync_button": "Sync Library & Trakt",
"syncing": "Syncing...",
"sync_desc": "Automatically syncs notifications for all shows in your library and Trakt watchlist/collection.",
"section_advanced": "Advanced",
"reset_button": "Reset All Notifications",
"test_button": "Test Notification (5 sec)",
"test_notification_in": "Notification in {{seconds}}s...",
"test_notification_text": "Notification will appear in {{seconds}} seconds",
"alert_reset_title": "Reset Notifications",
"alert_reset_msg": "This will cancel all scheduled notifications, but will not remove anything from your saved library. Are you sure?",
"alert_reset_success": "All notifications have been reset",
"alert_sync_complete": "Sync Complete",
"alert_sync_msg": "Successfully synced notifications for your library and Trakt items.\n\nScheduled: {{upcoming}} upcoming episodes\nThis week: {{thisWeek}} episodes",
"alert_test_scheduled": "Test notification scheduled to fire instantly"
},
"player": {
"title": "Video Player",
"section_selection": "PLAYER SELECTION",
"internal_title": "Built-in Player",
"internal_desc": "Use the app's default video player",
"vlc_title": "VLC",
"vlc_desc": "Open streams in VLC media player",
"infuse_title": "Infuse",
"infuse_desc": "Open streams in Infuse player",
"outplayer_title": "OutPlayer",
"outplayer_desc": "Open streams in OutPlayer",
"vidhub_title": "VidHub",
"vidhub_desc": "Open streams in VidHub player",
"infuse_live_title": "Infuse Livecontainer",
"infuse_live_desc": "Open streams in Infuse player LiveContainer",
"external_title": "External Player",
"external_desc": "Open streams in your preferred video player",
"section_playback": "PLAYBACK OPTIONS",
"autoplay_title": "Auto-play Best Stream",
"autoplay_desc": "Automatically start the highest quality stream available.",
"resume_title": "Always Resume",
"resume_desc": "Skip the resume prompt and automatically continue where you left off (if less than 85% watched).",
"engine_title": "Video Player Engine",
"engine_desc": "Auto uses ExoPlayer with MPV fallback. Some formats like Dolby Vision and HDR may not be supported by MPV, so Auto is recommended for best compatibility.",
"decoder_title": "Decoder Mode",
"decoder_desc": "How video is decoded. Auto is recommended for best balance.",
"gpu_title": "GPU Rendering",
"gpu_desc": "GPU-Next offers better HDR and color management.",
"external_downloads_title": "External Player for Downloads",
"external_downloads_desc": "Play downloaded content in your preferred external player.",
"restart_required": "Restart Required",
"restart_msg_decoder": "Please restart the app for the decoder change to take effect.",
"restart_msg_gpu": "Please restart the app for the GPU mode change to take effect."
},
"plugins": {
"title": "Plugins",
"enable_title": "Enable Plugins",
"enable_desc": "Allow the app to use installed plugins for finding streams",
"repo_config_title": "Repository Configuration",
"repo_config_desc": "Enable multiple repositories to combine plugins from different sources. Toggle each repository on or off below.",
"your_repos": "Your Repositories",
"your_repos_desc": "Enable multiple repositories to combine plugins from different sources.",
"add_repo_button": "Add Repository",
"refresh": "Refresh",
"remove": "Remove",
"enabled": "Enabled",
"disabled": "Disabled",
"updating": "Updating...",
"success": "Success",
"error": "Error",
"alert_repo_added": "Repository added and plugins loaded successfully",
"alert_repo_saved": "Repository URL saved successfully",
"alert_repo_refreshed": "Repository refreshed successfully with latest files",
"alert_invalid_url": "Invalid URL Format",
"alert_plugins_cleared": "All plugins have been removed",
"alert_cache_cleared": "Repository cache cleared successfully",
"unknown": "Unknown",
"active": "Active",
"available": "Available",
"platform_disabled": "Platform Disabled",
"limited": "Limited",
"clear_all": "Clear All Plugins",
"clear_all_desc": "Are you sure you want to remove all installed plugins? This action cannot be undone.",
"clear_cache": "Clear Repository Cache",
"clear_cache_desc": "This will remove the saved repository URL and clear all cached plugin data. You will need to re-enter your repository URL."
}
}

View file

@ -139,6 +139,321 @@
"external_first_desc": "Preferir legendas de addons, depois embutidas",
"any_available": "Qualquer Disponível",
"any_available_desc": "Usar primeira legenda disponível"
}
},
"trakt": "Trakt",
"clear_data": "Limpar Todos os Dados",
"clear_data_desc": "Isso redefinirá todas as configurações e limpará todos os dados em cache. Você tem certeza?",
"app_updates": "Atualizações do App",
"about_nuvio": "Sobre o Nuvio"
},
"ai_settings": {
"title": "Assistente IA",
"info_title": "Chat com IA",
"info_desc": "Faça perguntas sobre qualquer filme ou episódio usando IA avançada. Obtenha insights sobre enredo, personagens, temas, curiosidades e muito mais - tudo alimentado por dados abrangentes do TMDB.",
"feature_1": "Contexto e análise específica de episódios",
"feature_2": "Explicações de enredo e insights de personagens",
"feature_3": "Curiosidades e fatos de bastidores",
"feature_4": "Sua própria chave gratuita OpenRouter",
"api_key_section": "CHAVE API OPENROUTER",
"api_key_label": "Chave API",
"api_key_desc": "Digite sua chave OpenRouter para habilitar recursos de chat IA",
"save_api_key": "Salvar Chave API",
"saving": "Salvando...",
"update": "Atualizar",
"remove": "Remover",
"get_free_key": "Obter Chave Gratuita no OpenRouter",
"enable_chat": "Habilitar Chat IA",
"enable_chat_desc": "Quando habilitado, o botão Perguntar IA aparecerá nas páginas de conteúdo.",
"chat_enabled": "Chat IA Habilitado",
"chat_enabled_desc": "Você agora pode fazer perguntas sobre filmes e séries. Procure o botão \"Perguntar IA\" nas páginas de conteúdo!",
"how_it_works": "Como funciona",
"how_it_works_desc": "• OpenRouter fornece acesso a múltiplos modelos IA\n• Sua chave API permanece privada e segura\n• Camada gratuita inclui limites generosos de uso\n• Chat com contexto sobre episódios/filmes específicos\n• Obtenha análises detalhadas e explicações",
"error_invalid_key": "Por favor, digite uma chave API válida",
"error_key_format": "Chaves API OpenRouter devem começar com \"sk-or-\"",
"success_saved": "Chave API OpenRouter salva com sucesso!",
"error_save": "Falha ao salvar chave API",
"confirm_remove_title": "Remover Chave API",
"confirm_remove_msg": "Tem certeza que deseja remover sua chave API OpenRouter? Isso desativará os recursos de chat IA.",
"success_removed": "Chave API removida com sucesso",
"error_remove": "Falha ao remover chave API"
},
"catalog_settings": {
"title": "Catálogos",
"layout_phone": "LAYOUT DE CATÁLOGOS (CELULAR)",
"posters_per_row": "Pôsteres por linha",
"auto": "Auto",
"show_titles": "Mostrar Títulos",
"show_titles_desc": "Exibir título abaixo de cada pôster",
"phone_only_hint": "Aplica-se apenas a celulares. Tablets mantêm layout adaptativo.",
"catalogs_group": "Catálogos",
"enabled_count": "{{enabled}} de {{total}} habilitados",
"rename_hint": "Pressione e segure um catálogo para renomear",
"rename_modal_title": "Renomear Catálogo",
"rename_placeholder": "Digite o novo nome do catálogo",
"error_save_name": "Não foi possível salvar o nome personalizado."
},
"continue_watching_settings": {
"title": "Continuar Assistindo",
"playback_behavior": "COMPORTAMENTO DE REPRODUÇÃO",
"use_cached": "Usar Streams em Cache",
"use_cached_desc": "Quando habilitado, clicar em itens de Continuar Assistindo abrirá o player diretamente usando streams reproduzidos anteriormente. Quando desabilitado, abre uma tela de conteúdo.",
"open_metadata": "Abrir Tela de Metadados",
"open_metadata_desc": "Quando streams em cache estão desabilitados, abre a tela de Metadados em vez da tela de Streams. Isso mostra detalhes do conteúdo e permite seleção manual de streams.",
"card_appearance": "APARÊNCIA DO CARD",
"card_style": "Estilo do Card",
"card_style_desc": "Escolha como os itens de Continuar Assistindo aparecem na tela inicial",
"wide": "Largo",
"poster": "Pôster",
"cache_settings": "CONFIGURAÇÕES DE CACHE",
"cache_duration": "Duração do Cache de Stream",
"cache_duration_desc": "Por quanto tempo manter links de stream em cache antes de expirarem",
"important_note": "Nota Importante",
"important_note_text": "Nem todos os links de stream permanecem ativos por toda a duração do cache. Tempos de cache mais longos podem resultar em links expirados. Se um link em cache falhar, o app buscará novos streams.",
"how_it_works": "Como funciona",
"how_it_works_cached": "• Streams são armazenados em cache pela duração selecionada após a reprodução\n• Streams em cache são validados antes do uso\n• Se o cache for inválido ou expirado, volta para a tela de conteúdo\n• \"Usar Streams em Cache\" controla player direto vs navegação de tela\n• \"Abrir Tela de Metadados\" aparece apenas quando streams em cache estão desabilitados",
"how_it_works_uncached": "• Quando streams em cache estão desabilitados, clicar em itens de Continuar Assistindo abre telas de conteúdo\n• A opção \"Abrir Tela de Metadados\" controla qual tela abrir\n• A tela de Metadados mostra detalhes do conteúdo e permite seleção manual de streams\n• A tela de Streams mostra streams disponíveis para reprodução imediata",
"changes_saved": "Alterações salvas",
"min": "min",
"hour": "hora",
"sponsor_desc": "Patrocinou a infraestrutura do servidor para o Nuvio",
"mod_role": "Moderador do Discord",
"mod_desc": "Ajuda a moderar a comunidade do Nuvio no Discord",
"loading": "Carregando...",
"discord_user": "Usuário do Discord",
"contributions": "contribuições",
"gratitude_title": "Somos gratos por cada contribuição",
"gratitude_desc": "Cada linha de código, relatório de bug e sugestão ajuda a tornar o Nuvio melhor para todos",
"special_thanks_title": "Agradecimentos Especiais",
"special_thanks_desc": "Essas pessoas incríveis ajudam a manter a comunidade Nuvio funcionando e os servidores online",
"error_rate_limit": "Limite de taxa da API do GitHub excedido. Tente novamente mais tarde.",
"error_failed": "Falha ao carregar colaboradores. Verifique sua conexão com a internet.",
"retry": "Tentar Novamente",
"no_contributors": "Nenhum colaborador encontrado",
"loading_contributors": "Carregando colaboradores..."
},
"debrid": {
"title": "Integração Debrid",
"description_torbox": "Desbloqueie streams 4K de alta qualidade e velocidades ultra-rápidas integrando o Torbox. Insira sua chave API abaixo para atualizar instantaneamente sua experiência de streaming.",
"description_torrentio": "Configure o Torrentio para obter streams de torrent para filmes e séries. Um serviço debrid é necessário para transmitir conteúdo.",
"tab_torbox": "TorBox",
"tab_torrentio": "Torrentio",
"status_connected": "Conectado",
"status_disconnected": "Desconectado",
"enable_addon": "Ativar Addon",
"disconnect_button": "Desconectar e Remover",
"disconnect_loading": "Desconectando...",
"account_info": "Informações da Conta",
"plan": "Plano",
"plan_free": "Grátis",
"plan_essential": "Essencial ($3/mês)",
"plan_pro": "Pro ($10/mês)",
"plan_standard": "Padrão ($5/mês)",
"plan_unknown": "Desconhecido",
"expires": "Expira em",
"downloaded": "Baixado",
"status_active": "Ativo",
"connected_title": "✓ Conectado ao TorBox",
"connected_desc": "Seu addon TorBox está ativo e fornecendo streams premium.",
"configure_title": "Configurar Addon",
"configure_desc": "Personalize sua experiência. Ordene por qualidade, filtre tamanhos de arquivo e gerencie outras configurações.",
"open_settings": "Abrir Configurações",
"what_is_debrid": "O que é um Serviço Debrid?",
"enter_api_key": "Insira sua Chave API",
"connect_button": "Conectar e Instalar",
"connecting": "Conectando...",
"unlock_speeds_title": "Velocidades Premium",
"unlock_speeds_desc": "Assine o Torbox para acessar streams em cache de alta qualidade com zero buffering.",
"get_subscription": "Obter Assinatura",
"powered_by": "Desenvolvido por",
"disclaimer_torbox": "O Nuvio não é afiliado ao Torbox de nenhuma forma.",
"disclaimer_torrentio": "O Nuvio não é afiliado ao Torrentio de nenhuma forma.",
"installed_badge": "✓ INSTALADO",
"promo_title": "⚡ Precisa de um Serviço Debrid?",
"promo_desc": "Obtenha o TorBox para streaming 4K ultra-rápido com zero buffering. Torrents em cache premium e downloads instantâneos.",
"promo_button": "Assinar TorBox",
"service_label": "Serviço Debrid *",
"api_key_label": "Chave API *",
"sorting_label": "Ordenação",
"exclude_qualities": "Excluir Qualidades",
"priority_languages": "Idiomas Prioritários",
"max_results": "Máx. Resultados",
"additional_options": "Opções Adicionais",
"no_download_links": "Não mostrar links de download",
"no_debrid_catalog": "Não mostrar catálogo debrid",
"install_button": "Instalar Torrentio",
"installing": "Instalando...",
"update_button": "Atualizar Configuração",
"updating": "Atualizando...",
"remove_button": "Remover Torrentio",
"error_api_required": "Chave API Necessária",
"error_api_required_desc": "Insira a chave API do seu serviço debrid para instalar o Torrentio.",
"success_installed": "Addon Torrentio instalado com sucesso!",
"success_removed": "Addon Torrentio removido com sucesso",
"alert_disconnect_title": "Desconectar Torbox",
"alert_disconnect_msg": "Tem certeza que deseja desconectar o Torbox? Isso removerá o addon e limpará sua chave API salva."
},
"home_screen": {
"title": "Configurações da Tela Inicial",
"changes_applied": "Alterações Aplicadas",
"display_options": "OPÇÕES DE EXIBIÇÃO",
"show_hero": "Mostrar Seção Hero",
"show_hero_desc": "Conteúdo em destaque no topo",
"show_this_week": "Mostrar Seção Desta Semana",
"show_this_week_desc": "Novos episódios da semana atual",
"select_catalogs": "Selecionar Catálogos",
"all_catalogs": "Todos os catálogos",
"selected": "selecionados",
"hero_layout": "Layout do Hero",
"layout_legacy": "Legado",
"layout_carousel": "Carrossel",
"layout_appletv": "Apple TV",
"layout_desc": "Banner largura total, cartões deslizantes ou estilo Apple TV",
"featured_source": "Fonte de Destaques",
"using_catalogs": "Usando Catálogos",
"manage_catalogs": "Gerenciar catálogos selecionados",
"dynamic_bg": "Fundo Hero Dinâmico",
"dynamic_bg_desc": "Banner desfocado atrás do carrossel",
"performance_note": "Pode impactar o desempenho em dispositivos mais lentos.",
"posters": "Pôsteres",
"show_titles": "Mostrar Títulos",
"poster_size": "Tamanho do Pôster",
"poster_corners": "Cantos do Pôster",
"size_small": "Pequeno",
"size_medium": "Médio",
"size_large": "Grande",
"corner_square": "Quadrado",
"corner_rounded": "Arredondado",
"corner_pill": "Pílula",
"about_title": "SOBRE ESTAS CONFIGURAÇÕES",
"about_desc": "Estas configurações controlam como o conteúdo é exibido na sua tela inicial. As alterações são aplicadas imediatamente sem reiniciar o app."
},
"mdblist": {
"title": "Fontes de Avaliação",
"status_disabled": "MDBList Desativado",
"status_active": "Chave de API Ativa",
"status_required": "Chave de API Necessária",
"status_disabled_desc": "A funcionalidade MDBList está atualmente desativada.",
"status_active_desc": "As avaliações do MDBList estão ativadas.",
"status_required_desc": "Adicione sua chave abaixo para ativar as avaliações.",
"enable_toggle": "Ativar MDBList",
"enable_toggle_desc": "Ligar/desligar toda a funcionalidade do MDBList",
"api_section": "Chave de API",
"placeholder": "Cole sua chave de API MDBList",
"save": "Salvar",
"clear": "Limpar Chave",
"rating_providers": "Provedores de Avaliação",
"rating_providers_desc": "Escolha quais avaliações exibir no aplicativo",
"how_to": "Como obter uma chave de API",
"step_1": "Faça login no",
"step_1_link": "site do MDBList",
"step_2": "Vá para a seção",
"step_2_settings": "Configurações",
"step_2_api": "API",
"step_2_end": ".",
"step_3": "Gere uma nova chave e copie-a.",
"go_to_website": "Ir para o MDBList",
"alert_clear_title": "Limpar Chave de API",
"alert_clear_msg": "Tem certeza de que deseja remover a chave de API salva?",
"success_saved": "Chave de API salva com sucesso.",
"error_empty": "Chave API não pode estar vazia.",
"error_save": "Ocorreu um erro ao salvar. Por favor, tente novamente.",
"api_key_empty_error": "A Chave de API não pode estar vazia.",
"success_cleared": "Chave de API limpa com sucesso",
"error_clear": "Falha ao limpar a chave de API"
},
"notification": {
"title": "Configurações de Notificação",
"section_general": "Geral",
"enable_notifications": "Ativar Notificações",
"section_types": "Tipos de Notificação",
"new_episodes": "Novos Episódios",
"upcoming_shows": "Próximos Programas",
"reminders": "Lembretes",
"section_timing": "Tempo de Notificação",
"timing_desc": "Quanto tempo antes de um episódio ir ao ar você deve ser notificado?",
"hours_1": "1 hora",
"hours_suffix": "horas",
"section_status": "Status da Notificação",
"stats_upcoming": "Próximos",
"stats_this_week": "Esta Semana",
"stats_total": "Total",
"sync_button": "Sincronizar Biblioteca e Trakt",
"syncing": "Sincronizando...",
"sync_desc": "Sincroniza automaticamente notificações para todos os programas na sua biblioteca e watchlist/coleção do Trakt.",
"section_advanced": "Avançado",
"reset_button": "Redefinir Todas as Notificações",
"test_button": "Notificação de Teste (5 seg)",
"test_notification_in": "Notificação em {{seconds}}s...",
"test_notification_text": "A notificação aparecerá em {{seconds}} segundos",
"alert_reset_title": "Redefinir Notificações",
"alert_reset_msg": "Isso cancelará todas as notificações agendadas, mas não removerá nada da sua biblioteca salva. Tem certeza?",
"alert_reset_success": "Todas as notificações foram redefinidas",
"alert_sync_complete": "Sincronização Completa",
"alert_sync_msg": "Notificações sincronizadas com sucesso para sua biblioteca e itens do Trakt.\n\nAgendadas: {{upcoming}} próximos episódios\nEsta semana: {{thisWeek}} episódios",
"alert_test_scheduled": "Notificação de teste agendada para disparar instantaneamente"
},
"player": {
"title": "Player de Vídeo",
"section_selection": "SELEÇÃO DE PLAYER",
"internal_title": "Player Integrado",
"internal_desc": "Usar o player de vídeo padrão do aplicativo",
"vlc_title": "VLC",
"vlc_desc": "Abrir streams no player de mídia VLC",
"infuse_title": "Infuse",
"infuse_desc": "Abrir streams no player Infuse",
"outplayer_title": "OutPlayer",
"outplayer_desc": "Abrir streams no OutPlayer",
"vidhub_title": "VidHub",
"vidhub_desc": "Abrir streams no player VidHub",
"infuse_live_title": "Infuse Livecontainer",
"infuse_live_desc": "Abrir streams no player Infuse LiveContainer",
"external_title": "Player Externo",
"external_desc": "Abrir streams no seu player de vídeo preferido",
"section_playback": "OPÇÕES DE REPRODUÇÃO",
"autoplay_title": "Reprodução Automática (Melhor Stream)",
"autoplay_desc": "Iniciar automaticamente o stream de melhor qualidade disponível.",
"resume_title": "Sempre Retomar",
"resume_desc": "Pular o aviso de retomar e continuar automaticamente de onde parou (se assistido menos de 85%).",
"engine_title": "Motor do Player de Vídeo",
"engine_desc": "Auto usa ExoPlayer com fallback para MPV. Alguns formatos como Dolby Vision e HDR podem não ser suportados pelo MPV, então Auto é recomendado para melhor compatibilidade.",
"decoder_title": "Modo de Decodificador",
"decoder_desc": "Como o vídeo é decodificado. Auto é recomendado para melhor equilíbrio.",
"gpu_title": "Renderização GPU",
"gpu_desc": "GPU-Next oferece melhor HDR e gerenciamento de cores.",
"external_downloads_title": "Player Externo para Downloads",
"external_downloads_desc": "Reproduzir conteúdo baixado no seu player externo preferido.",
"restart_required": "Reinicialização Necessária"
},
"plugins": {
"title": "Plugins",
"enable_title": "Ativar Plugins",
"enable_desc": "Permitir que o aplicativo use plugins instalados para encontrar transmissões",
"repo_config_title": "Configuração de Repositório",
"repo_config_desc": "Ative vários repositórios para combinar plugins de diferentes fontes. Ative ou desative cada repositório abaixo.",
"your_repos": "Seus Repositórios",
"your_repos_desc": "Ative vários repositórios para combinar plugins de diferentes fontes.",
"add_repo_button": "Adicionar Repositório",
"refresh": "Atualizar",
"remove": "Remover",
"enabled": "Ativado",
"disabled": "Desativado",
"updating": "Atualizando...",
"success": "Sucesso",
"error": "Erro",
"alert_repo_added": "Repositório adicionado e plugins carregados com sucesso",
"alert_repo_saved": "URL do repositório salvo com sucesso",
"alert_repo_refreshed": "Repositório atualizado com sucesso com arquivos mais recentes",
"alert_invalid_url": "Formato de URL inválido",
"alert_plugins_cleared": "Todos os plugins foram removidos",
"alert_cache_cleared": "Cache do repositório limpo com sucesso",
"unknown": "Desconhecido",
"active": "Ativo",
"available": "Disponível",
"platform_disabled": "Plataforma Desativada",
"limited": "Limitado",
"clear_all": "Limpar Todos os Plugins",
"clear_all_desc": "Você tem certeza de que deseja remover todos os plugins instalados? Esta ação não pode ser desfeita.",
"clear_cache": "Limpar Cache do Repositório",
"clear_cache_desc": "Isso removerá a URL do repositório salva e limpará todos os dados de plugins armazenados em cache. Você precisará digitar a URL do repositório novamente."
}
}

View file

@ -21,11 +21,13 @@ import { useTheme } from '../contexts/ThemeContext';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useSettings } from '../hooks/useSettings';
import { SvgXml } from 'react-native-svg';
import { useTranslation } from 'react-i18next';
const { width } = Dimensions.get('window');
const isTablet = width >= 768;
const AISettingsScreen: React.FC = () => {
const { t } = useTranslation();
// CustomAlert state (must be inside the component)
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
@ -69,7 +71,7 @@ const AISettingsScreen: React.FC = () => {
<path stroke-width=".4" d="m244.1 250.4-60.3-34.7v69.5l60.3-34.8Z"/>
</g>
</svg>`;
const [apiKey, setApiKey] = useState('');
const [loading, setLoading] = useState(false);
const [isKeySet, setIsKeySet] = useState(false);
@ -92,12 +94,12 @@ const AISettingsScreen: React.FC = () => {
const handleSaveApiKey = async () => {
if (!apiKey.trim()) {
openAlert('Error', 'Please enter a valid API key');
openAlert(t('common.error'), t('ai_settings.error_invalid_key'));
return;
}
if (!apiKey.startsWith('sk-or-')) {
openAlert('Error', 'OpenRouter API keys should start with "sk-or-"');
openAlert(t('common.error'), t('ai_settings.error_key_format'));
return;
}
@ -105,9 +107,9 @@ const AISettingsScreen: React.FC = () => {
try {
await mmkvStorage.setItem('openrouter_api_key', apiKey.trim());
setIsKeySet(true);
openAlert('Success', 'OpenRouter API key saved successfully!');
openAlert(t('common.success'), t('ai_settings.success_saved'));
} catch (error) {
openAlert('Error', 'Failed to save API key');
openAlert(t('common.error'), t('ai_settings.error_save'));
if (__DEV__) console.error('Error saving OpenRouter API key:', error);
} finally {
setLoading(false);
@ -116,10 +118,10 @@ const AISettingsScreen: React.FC = () => {
const handleRemoveApiKey = () => {
openAlert(
'Remove API Key',
'Are you sure you want to remove your OpenRouter API key? This will disable AI chat features.',
t('ai_settings.confirm_remove_title'),
t('ai_settings.confirm_remove_msg'),
[
{ label: 'Cancel', onPress: () => {} },
{ label: t('common.cancel'), onPress: () => { } },
{
label: 'Remove',
onPress: async () => {
@ -127,9 +129,9 @@ const AISettingsScreen: React.FC = () => {
await mmkvStorage.removeItem('openrouter_api_key');
setApiKey('');
setIsKeySet(false);
openAlert('Success', 'API key removed successfully');
openAlert(t('common.success'), t('ai_settings.success_removed'));
} catch (error) {
openAlert('Error', 'Failed to remove API key');
openAlert(t('common.error'), t('ai_settings.error_remove'));
}
}
}
@ -142,35 +144,35 @@ const AISettingsScreen: React.FC = () => {
};
return (
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" />
{/* Header */}
<View style={styles.header}>
<TouchableOpacity
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backButton}
>
<MaterialIcons
name="arrow-back"
size={24}
color={currentTheme.colors.text}
<MaterialIcons
name="arrow-back"
size={24}
color={currentTheme.colors.text}
/>
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
Settings
{t('settings.settings_title')}
</Text>
</TouchableOpacity>
<View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */}
</View>
</View>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
AI Assistant
{t('ai_settings.title')}
</Text>
<ScrollView
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
@ -178,42 +180,42 @@ const AISettingsScreen: React.FC = () => {
{/* Info Card */}
<View style={[styles.infoCard, { backgroundColor: currentTheme.colors.elevation1 }]}>
<View style={styles.infoHeader}>
<MaterialIcons
name="smart-toy"
size={24}
<MaterialIcons
name="smart-toy"
size={24}
color={currentTheme.colors.primary}
/>
<Text style={[styles.infoTitle, { color: currentTheme.colors.highEmphasis }]}>
AI-Powered Chat
{t('ai_settings.info_title')}
</Text>
</View>
<Text style={[styles.infoDescription, { color: currentTheme.colors.mediumEmphasis }]}>
Ask questions about any movie or TV show episode using advanced AI. Get insights about plot, characters, themes, trivia, and more - all powered by comprehensive TMDB data.
{t('ai_settings.info_desc')}
</Text>
<View style={styles.featureList}>
<View style={styles.featureItem}>
<MaterialIcons name="check-circle" size={16} color={currentTheme.colors.primary} />
<Text style={[styles.featureText, { color: currentTheme.colors.mediumEmphasis }]}>
Episode-specific context and analysis
{t('ai_settings.feature_1')}
</Text>
</View>
<View style={styles.featureItem}>
<MaterialIcons name="check-circle" size={16} color={currentTheme.colors.primary} />
<Text style={[styles.featureText, { color: currentTheme.colors.mediumEmphasis }]}>
Plot explanations and character insights
{t('ai_settings.feature_2')}
</Text>
</View>
<View style={styles.featureItem}>
<MaterialIcons name="check-circle" size={16} color={currentTheme.colors.primary} />
<Text style={[styles.featureText, { color: currentTheme.colors.mediumEmphasis }]}>
Behind-the-scenes trivia and facts
{t('ai_settings.feature_3')}
</Text>
</View>
<View style={styles.featureItem}>
<MaterialIcons name="check-circle" size={16} color={currentTheme.colors.primary} />
<Text style={[styles.featureText, { color: currentTheme.colors.mediumEmphasis }]}>
Your own free OpenRouter API key
{t('ai_settings.feature_4')}
</Text>
</View>
</View>
@ -222,21 +224,21 @@ const AISettingsScreen: React.FC = () => {
{/* API Key Configuration */}
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1 }]}>
<Text style={[styles.cardTitle, { color: currentTheme.colors.mediumEmphasis }]}>
OPENROUTER API KEY
{t('ai_settings.api_key_section')}
</Text>
<View style={styles.apiKeySection}>
<Text style={[styles.label, { color: currentTheme.colors.highEmphasis }]}>
API Key
{t('ai_settings.api_key_label')}
</Text>
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis }]}>
Enter your OpenRouter API key to enable AI chat features
{t('ai_settings.api_key_desc')}
</Text>
<TextInput
style={[
styles.input,
{
{
backgroundColor: currentTheme.colors.elevation2,
color: currentTheme.colors.highEmphasis,
borderColor: currentTheme.colors.elevation2
@ -258,14 +260,14 @@ const AISettingsScreen: React.FC = () => {
onPress={handleSaveApiKey}
disabled={loading}
>
<MaterialIcons
name="save"
size={20}
<MaterialIcons
name="save"
size={20}
color={currentTheme.colors.white}
style={{ marginRight: 8 }}
/>
<Text style={styles.saveButtonText}>
{loading ? 'Saving...' : 'Save API Key'}
{loading ? t('ai_settings.saving') : t('ai_settings.save_api_key')}
</Text>
</TouchableOpacity>
) : (
@ -275,27 +277,27 @@ const AISettingsScreen: React.FC = () => {
onPress={handleSaveApiKey}
disabled={loading}
>
<MaterialIcons
name="update"
size={20}
<MaterialIcons
name="update"
size={20}
color={currentTheme.colors.white}
style={{ marginRight: 8 }}
/>
<Text style={styles.updateButtonText}>Update</Text>
<Text style={styles.updateButtonText}>{t('ai_settings.update')}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.removeButton, { borderColor: currentTheme.colors.error }]}
onPress={handleRemoveApiKey}
>
<MaterialIcons
name="delete"
size={20}
<MaterialIcons
name="delete"
size={20}
color={currentTheme.colors.error}
style={{ marginRight: 8 }}
/>
<Text style={[styles.removeButtonText, { color: currentTheme.colors.error }]}>
Remove
{t('ai_settings.remove')}
</Text>
</TouchableOpacity>
</View>
@ -306,23 +308,23 @@ const AISettingsScreen: React.FC = () => {
style={[styles.getKeyButton, { backgroundColor: currentTheme.colors.elevation2 }]}
onPress={handleGetApiKey}
>
<MaterialIcons
name="open-in-new"
size={20}
<MaterialIcons
name="open-in-new"
size={20}
color={currentTheme.colors.primary}
style={{ marginRight: 8 }}
/>
<Text style={[styles.getKeyButtonText, { color: currentTheme.colors.primary }]}>
Get Free API Key from OpenRouter
{t('ai_settings.get_free_key')}
</Text>
</TouchableOpacity>
</View>
</View>
{/* Enable Toggle (top) */}
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1 }]}>
<View style={[styles.card, { backgroundColor: currentTheme.colors.elevation1 }]}>
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
<Text style={[styles.label, { color: currentTheme.colors.highEmphasis }]}>Enable AI Chat</Text>
<Text style={[styles.label, { color: currentTheme.colors.highEmphasis }]}>{t('ai_settings.enable_chat')}</Text>
<Switch
value={!!settings.aiChatEnabled}
onValueChange={(v) => updateSetting('aiChatEnabled', v)}
@ -331,24 +333,24 @@ const AISettingsScreen: React.FC = () => {
ios_backgroundColor={currentTheme.colors.elevation2}
/>
</View>
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis, marginTop: 8 }]}>When enabled, the Ask AI button will appear on content pages.</Text>
<Text style={[styles.description, { color: currentTheme.colors.mediumEmphasis, marginTop: 8 }]}>{t('ai_settings.enable_chat_desc')}</Text>
</View>
{/* Status Card */}
{isKeySet && (
<View style={[styles.statusCard, { backgroundColor: currentTheme.colors.elevation1 }]}>
<View style={styles.statusHeader}>
<MaterialIcons
name="check-circle"
size={24}
<MaterialIcons
name="check-circle"
size={24}
color={currentTheme.colors.success || '#4CAF50'}
/>
<Text style={[styles.statusTitle, { color: currentTheme.colors.success || '#4CAF50' }]}>
AI Chat Enabled
{t('ai_settings.chat_enabled')}
</Text>
</View>
<Text style={[styles.statusDescription, { color: currentTheme.colors.mediumEmphasis }]}>
You can now ask questions about movies and TV shows. Look for the "Ask AI" button on content pages!
{t('ai_settings.chat_enabled_desc')}
</Text>
</View>
)}
@ -356,14 +358,10 @@ const AISettingsScreen: React.FC = () => {
{/* Usage Info */}
<View style={[styles.usageCard, { backgroundColor: currentTheme.colors.elevation1 }]}>
<Text style={[styles.usageTitle, { color: currentTheme.colors.highEmphasis }]}>
How it works
{t('ai_settings.how_it_works')}
</Text>
<Text style={[styles.usageText, { color: currentTheme.colors.mediumEmphasis }]}>
OpenRouter provides access to multiple AI models{'\n'}
Your API key stays private and secure{'\n'}
Free tier includes generous usage limits{'\n'}
Chat with context about specific episodes/movies{'\n'}
Get detailed analysis and explanations
{t('ai_settings.how_it_works_desc')}
</Text>
</View>
{/* OpenRouter branding */}

View file

@ -664,9 +664,9 @@ const AddonsScreen = () => {
setAlertVisible(true);
} catch (error) {
logger.error('Failed to install addon:', error);
setAlertTitle('Error');
setAlertMessage('Failed to install addon');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertTitle(t('common.error'));
setAlertMessage(t('addons.install_error'));
setAlertActions([{ label: t('common.ok'), onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} finally {
setInstalling(false);
@ -909,7 +909,7 @@ const AddonsScreen = () => {
)}
</View>
<View style={styles.addonMetaContainer}>
<Text style={styles.addonVersion}>v{item.version || '1.0.0'}</Text>
<Text style={styles.addonVersion}>{t('addons.version', { version: item.version || '1.0.0' })}</Text>
<Text style={styles.addonDot}></Text>
<Text style={styles.addonCategory}>{categoryText}</Text>
</View>
@ -967,7 +967,7 @@ const AddonsScreen = () => {
onPress={() => navigation.goBack()}
>
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
<Text style={styles.backText}>Settings</Text>
<Text style={styles.backText}>{t('settings.settings_title')}</Text>
</TouchableOpacity>
<View style={styles.headerActions}>
@ -1027,18 +1027,18 @@ const AddonsScreen = () => {
<View style={styles.section}>
<Text style={styles.sectionTitle}>OVERVIEW</Text>
<View style={styles.statsContainer}>
<StatsCard value={addons.length} label="Addons" />
<StatsCard value={addons.length} label={t('addons.title')} />
<View style={styles.statsDivider} />
<StatsCard value={addons.length} label="Active" />
<StatsCard value={addons.length} label={t('settings.items.active')} />
<View style={styles.statsDivider} />
<StatsCard value={catalogCount} label="Catalogs" />
<StatsCard value={catalogCount} label={t('settings.items.catalogs')} />
</View>
</View>
{/* Hide Add Addon Section in reorder mode */}
{!reorderMode && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>ADD NEW ADDON</Text>
<Text style={styles.sectionTitle}>{t('addons.add_button').toUpperCase()}</Text>
<View style={styles.addAddonContainer}>
<TextInput
style={styles.addonInput}
@ -1055,7 +1055,7 @@ const AddonsScreen = () => {
disabled={installing || !addonUrl}
>
<Text style={styles.addButtonText}>
{installing ? 'Loading...' : t('addons.add_button')}
{installing ? t('common.loading') : t('addons.add_button')}
</Text>
</TouchableOpacity>
</View>
@ -1115,7 +1115,7 @@ const AddonsScreen = () => {
{addonDetails && (
<>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Install Addon</Text>
<Text style={styles.modalTitle}>{t('addons.install')}</Text>
<TouchableOpacity
onPress={() => {
setShowConfirmModal(false);
@ -1145,7 +1145,7 @@ const AddonsScreen = () => {
</View>
)}
<Text style={styles.addonDetailName}>{addonDetails.name}</Text>
<Text style={styles.addonDetailVersion}>v{addonDetails.version || '1.0.0'}</Text>
<Text style={styles.addonDetailVersion}>{t('addons.version', { version: addonDetails.version || '1.0.0' })}</Text>
</View>
<View style={styles.addonDetailSection}>

View file

@ -25,6 +25,7 @@ import { logger } from '../utils/logger';
import { clearCustomNameCache } from '../utils/catalogNameUtils';
import { BlurView } from 'expo-blur';
import CustomAlert from '../components/CustomAlert';
import { useTranslation } from 'react-i18next';
// Optional iOS Glass effect (expo-glass-effect) with safe fallback for CatalogSettingsScreen
let GlassViewComp: any = null;
@ -275,6 +276,7 @@ const CatalogSettingsScreen = () => {
const colors = currentTheme.colors;
const styles = createStyles(colors);
const isDarkMode = true; // Force dark mode
const { t } = useTranslation();
// Modal State
const [isRenameModalVisible, setIsRenameModalVisible] = useState(false);
@ -489,9 +491,9 @@ const CatalogSettingsScreen = () => {
} catch (error) {
logger.error('Failed to save custom catalog name:', error);
setAlertTitle('Error');
setAlertMessage('Could not save the custom name.');
setAlertActions([{ label: 'OK', onPress: () => { } }]);
setAlertTitle(t('common.error'));
setAlertMessage(t('catalog_settings.error_save_name'));
setAlertActions([{ label: t('common.ok'), onPress: () => { } }]);
setAlertVisible(true);
} finally {
setIsRenameModalVisible(false);
@ -514,10 +516,10 @@ const CatalogSettingsScreen = () => {
onPress={() => navigation.goBack()}
>
<MaterialIcons name="chevron-left" size={28} color={colors.primary} />
<Text style={styles.backText}>Settings</Text>
<Text style={styles.backText}>{t('settings.settings_title')}</Text>
</TouchableOpacity>
</View>
<Text style={styles.headerTitle}>Catalogs</Text>
<Text style={styles.headerTitle}>{t('catalog_settings.title')}</Text>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
@ -534,19 +536,19 @@ const CatalogSettingsScreen = () => {
onPress={() => navigation.goBack()}
>
<MaterialIcons name="chevron-left" size={28} color={colors.primary} />
<Text style={styles.backText}>Settings</Text>
<Text style={styles.backText}>{t('settings.settings_title')}</Text>
</TouchableOpacity>
</View>
<Text style={styles.headerTitle}>Catalogs</Text>
<Text style={styles.headerTitle}>{t('catalog_settings.title')}</Text>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent}>
{/* Layout (Mobile only) */}
{Platform.OS && (
<View style={styles.addonSection}>
<Text style={styles.addonTitle}>LAYOUT CATALOGSCREEN (PHONE)</Text>
<Text style={styles.addonTitle}>{t('catalog_settings.layout_phone')}</Text>
<View style={styles.card}>
<View style={styles.groupHeader}>
<Text style={styles.groupTitle}>Posters per row</Text>
<Text style={styles.groupTitle}>{t('catalog_settings.posters_per_row')}</Text>
<View style={styles.groupHeaderRight} />
</View>
{/* Only show on phones (approx width < 600) */}
@ -561,7 +563,7 @@ const CatalogSettingsScreen = () => {
}}
activeOpacity={0.7}
>
<Text style={[styles.optionChipText, mobileColumns === 'auto' && styles.optionChipTextSelected]}>Auto</Text>
<Text style={[styles.optionChipText, mobileColumns === 'auto' && styles.optionChipTextSelected]}>{t('catalog_settings.auto')}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.optionChip, mobileColumns === 2 && styles.optionChipSelected]}
@ -590,14 +592,14 @@ const CatalogSettingsScreen = () => {
</View>
<View style={styles.hintRow}>
<MaterialIcons name="info-outline" size={14} color={colors.mediumGray} />
<Text style={styles.hintText}>Applies to phones only. Tablets keep adaptive layout.</Text>
<Text style={styles.hintText}>{t('catalog_settings.phone_only_hint')}</Text>
</View>
{/* Show Titles Toggle */}
<View style={[styles.catalogItem, { borderBottomWidth: 0 }]}>
<View style={styles.catalogInfo}>
<Text style={styles.catalogName}>Show Poster Titles</Text>
<Text style={styles.catalogType}>Display title text below each poster</Text>
<Text style={styles.catalogName}>{t('catalog_settings.show_titles')}</Text>
<Text style={styles.catalogType}>{t('catalog_settings.show_titles_desc')}</Text>
</View>
<Switch
value={showTitles}
@ -628,10 +630,10 @@ const CatalogSettingsScreen = () => {
onPress={() => toggleExpansion(addonId)}
activeOpacity={0.7}
>
<Text style={styles.groupTitle}>Catalogs</Text>
<Text style={styles.groupTitle}>{t('catalog_settings.catalogs_group')}</Text>
<View style={styles.groupHeaderRight}>
<Text style={styles.enabledCount}>
{group.enabledCount} of {group.catalogs.length} enabled
{t('catalog_settings.enabled_count', { enabled: group.enabledCount, total: group.catalogs.length })}
</Text>
<MaterialIcons
name={group.expanded ? "keyboard-arrow-down" : "keyboard-arrow-right"}
@ -645,7 +647,7 @@ const CatalogSettingsScreen = () => {
<>
<View style={styles.hintRow}>
<MaterialIcons name="edit" size={14} color={colors.mediumGray} />
<Text style={styles.hintText}>Long-press a catalog to rename</Text>
<Text style={styles.hintText}>{t('catalog_settings.rename_hint')}</Text>
</View>
{group.catalogs.map((setting, index) => (
<Pressable
@ -696,36 +698,36 @@ const CatalogSettingsScreen = () => {
{GlassViewComp && liquidGlassAvailable ? (
<GlassViewComp style={styles.modalContent} glassEffectStyle="regular">
<Pressable onPress={(e) => e.stopPropagation()}>
<Text style={styles.modalTitle}>Rename Catalog</Text>
<Text style={styles.modalTitle}>{t('catalog_settings.rename_modal_title')}</Text>
<TextInput
style={styles.modalInput}
value={currentRenameValue}
onChangeText={setCurrentRenameValue}
placeholder="Enter new catalog name"
placeholder={t('catalog_settings.rename_placeholder')}
placeholderTextColor={colors.mediumGray}
autoFocus={true}
/>
<View style={styles.modalButtons}>
<Button title="Cancel" onPress={() => setIsRenameModalVisible(false)} color={colors.mediumGray} />
<Button title="Save" onPress={handleSaveRename} color={colors.primary} />
<Button title={t('common.cancel')} onPress={() => setIsRenameModalVisible(false)} color={colors.mediumGray} />
<Button title={t('common.save')} onPress={handleSaveRename} color={colors.primary} />
</View>
</Pressable>
</GlassViewComp>
) : (
<BlurView style={styles.modalContent} intensity={90} tint="default">
<Pressable onPress={(e) => e.stopPropagation()}>
<Text style={styles.modalTitle}>Rename Catalog</Text>
<Text style={styles.modalTitle}>{t('catalog_settings.rename_modal_title')}</Text>
<TextInput
style={styles.modalInput}
value={currentRenameValue}
onChangeText={setCurrentRenameValue}
placeholder="Enter new catalog name"
placeholder={t('catalog_settings.rename_placeholder')}
placeholderTextColor={colors.mediumGray}
autoFocus={true}
/>
<View style={styles.modalButtons}>
<Button title="Cancel" onPress={() => setIsRenameModalVisible(false)} color={colors.mediumGray} />
<Button title="Save" onPress={handleSaveRename} color={colors.primary} />
<Button title={t('common.cancel')} onPress={() => setIsRenameModalVisible(false)} color={colors.mediumGray} />
<Button title={t('common.save')} onPress={handleSaveRename} color={colors.primary} />
</View>
</Pressable>
</BlurView>
@ -734,18 +736,18 @@ const CatalogSettingsScreen = () => {
) : (
<Pressable style={styles.modalOverlay} onPress={() => setIsRenameModalVisible(false)}>
<Pressable style={styles.modalContent} onPress={(e) => e.stopPropagation()}>
<Text style={styles.modalTitle}>Rename Catalog</Text>
<Text style={styles.modalTitle}>{t('catalog_settings.rename_modal_title')}</Text>
<TextInput
style={styles.modalInput}
value={currentRenameValue}
onChangeText={setCurrentRenameValue}
placeholder="Enter new catalog name"
placeholder={t('catalog_settings.rename_placeholder')}
placeholderTextColor={colors.mediumGray}
autoFocus={true}
/>
<View style={styles.modalButtons}>
<Button title="Cancel" onPress={() => setIsRenameModalVisible(false)} color={colors.mediumGray} />
<Button title="Save" onPress={handleSaveRename} color={colors.primary} />
<Button title={t('common.cancel')} onPress={() => setIsRenameModalVisible(false)} color={colors.mediumGray} />
<Button title={t('common.save')} onPress={handleSaveRename} color={colors.primary} />
</View>
</Pressable>
</Pressable>

View file

@ -17,23 +17,9 @@ import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
import { useSettings } from '../hooks/useSettings';
import { RootStackParamList } from '../navigation/AppNavigator';
import { useTranslation } from 'react-i18next';
// TTL options in milliseconds - organized in rows
const TTL_OPTIONS = [
[
{ label: '15 min', value: 15 * 60 * 1000 },
{ label: '30 min', value: 30 * 60 * 1000 },
{ label: '1 hour', value: 60 * 60 * 1000 },
],
[
{ label: '2 hours', value: 2 * 60 * 60 * 1000 },
{ label: '6 hours', value: 6 * 60 * 60 * 1000 },
{ label: '12 hours', value: 12 * 60 * 60 * 1000 },
],
[
{ label: '24 hours', value: 24 * 60 * 60 * 1000 },
],
];
const ContinueWatchingSettingsScreen: React.FC = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
@ -43,6 +29,24 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
const styles = createStyles(colors);
const [showSavedIndicator, setShowSavedIndicator] = useState(false);
const fadeAnim = React.useRef(new Animated.Value(0)).current;
const { t } = useTranslation();
// TTL options in milliseconds - organized in rows
const TTL_OPTIONS = [
[
{ label: `15 ${t('continue_watching_settings.min')}`, value: 15 * 60 * 1000 },
{ label: `30 ${t('continue_watching_settings.min')}`, value: 30 * 60 * 1000 },
{ label: `1 ${t('continue_watching_settings.hour')}`, value: 60 * 60 * 1000 },
],
[
{ label: `2 ${t('continue_watching_settings.hours')}`, value: 2 * 60 * 60 * 1000 },
{ label: `6 ${t('continue_watching_settings.hours')}`, value: 6 * 60 * 60 * 1000 },
{ label: `12 ${t('continue_watching_settings.hours')}`, value: 12 * 60 * 60 * 1000 },
],
[
{ label: `24 ${t('continue_watching_settings.hours')}`, value: 24 * 60 * 60 * 1000 },
],
];
// Prevent iOS entrance flicker by restoring a non-translucent StatusBar
useEffect(() => {
@ -167,12 +171,12 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
onPress={handleBack}
>
<MaterialIcons name="chevron-left" size={28} color={colors.white} />
<Text style={styles.backText}>Settings</Text>
<Text style={styles.backText}>{t('settings.settings_title')}</Text>
</TouchableOpacity>
</View>
<Text style={styles.headerTitle}>
Continue Watching
{t('continue_watching_settings.title')}
</Text>
{/* Content */}
@ -182,19 +186,19 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
contentContainerStyle={styles.contentContainer}
>
<View style={styles.section}>
<Text style={styles.sectionTitle}>PLAYBACK BEHAVIOR</Text>
<Text style={styles.sectionTitle}>{t('continue_watching_settings.playback_behavior')}</Text>
<View style={styles.settingsCard}>
<SettingItem
title="Use Cached Streams"
description="When enabled, clicking Continue Watching items will open the player directly using previously played streams. When disabled, opens a content screen instead."
title={t('continue_watching_settings.use_cached')}
description={t('continue_watching_settings.use_cached_desc')}
value={settings.useCachedStreams}
onValueChange={(value) => handleUpdateSetting('useCachedStreams', value)}
isLast={!settings.useCachedStreams}
/>
{!settings.useCachedStreams && (
<SettingItem
title="Open Metadata Screen"
description="When cached streams are disabled, open the Metadata screen instead of the Streams screen. This shows content details and allows manual stream selection."
title={t('continue_watching_settings.open_metadata')}
description={t('continue_watching_settings.open_metadata_desc')}
value={settings.openMetadataScreenWhenCacheDisabled}
onValueChange={(value) => handleUpdateSetting('openMetadataScreenWhenCacheDisabled', value)}
isLast={true}
@ -205,14 +209,14 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
{/* Card Appearance Section */}
<View style={styles.section}>
<Text style={styles.sectionTitle}>CARD APPEARANCE</Text>
<Text style={styles.sectionTitle}>{t('continue_watching_settings.card_appearance')}</Text>
<View style={styles.settingsCard}>
<View style={[styles.settingItem, { borderBottomWidth: 0, flexDirection: 'column', alignItems: 'flex-start' }]}>
<Text style={[styles.settingTitle, { color: colors.highEmphasis, marginBottom: 8 }]}>
Card Style
{t('continue_watching_settings.card_style')}
</Text>
<Text style={[styles.settingDescription, { color: colors.mediumEmphasis, marginBottom: 16 }]}>
Choose how Continue Watching items appear on the home screen
{t('continue_watching_settings.card_style_desc')}
</Text>
<View style={styles.cardStyleOptionsContainer}>
<TouchableOpacity
@ -240,7 +244,7 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
styles.cardStyleLabel,
{ color: settings.continueWatchingCardStyle === 'wide' ? colors.white : colors.highEmphasis }
]}>
Wide
{t('continue_watching_settings.wide')}
</Text>
{settings.continueWatchingCardStyle === 'wide' && (
<MaterialIcons name="check-circle" size={18} color={colors.white} style={styles.cardStyleCheck} />
@ -268,7 +272,7 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
styles.cardStyleLabel,
{ color: settings.continueWatchingCardStyle === 'poster' ? colors.white : colors.highEmphasis }
]}>
Poster
{t('continue_watching_settings.poster')}
</Text>
{settings.continueWatchingCardStyle === 'poster' && (
<MaterialIcons name="check-circle" size={18} color={colors.white} style={styles.cardStyleCheck} />
@ -281,14 +285,14 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
{settings.useCachedStreams && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>CACHE SETTINGS</Text>
<Text style={styles.sectionTitle}>{t('continue_watching_settings.cache_settings')}</Text>
<View style={styles.settingsCard}>
<View style={[styles.settingItem, { borderBottomWidth: 0, flexDirection: 'column', alignItems: 'flex-start' }]}>
<Text style={[styles.settingTitle, { color: colors.highEmphasis, marginBottom: 8 }]}>
Stream Cache Duration
{t('continue_watching_settings.cache_duration')}
</Text>
<Text style={[styles.settingDescription, { color: colors.mediumEmphasis, marginBottom: 16 }]}>
How long to keep cached stream links before they expire
{t('continue_watching_settings.cache_duration_desc')}
</Text>
<View style={styles.ttlOptionsContainer}>
{TTL_OPTIONS.map((row, rowIndex) => (
@ -310,11 +314,11 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
<View style={styles.warningHeader}>
<MaterialIcons name="warning" size={20} color={colors.warning} />
<Text style={[styles.warningTitle, { color: colors.warning }]}>
Important Note
{t('continue_watching_settings.important_note')}
</Text>
</View>
<Text style={[styles.warningText, { color: colors.mediumEmphasis }]}>
Not all stream links may remain active for the full cache duration. Longer cache times may result in expired links. If a cached link fails, the app will fall back to fetching fresh streams.
{t('continue_watching_settings.important_note_text')}
</Text>
</View>
</View>
@ -325,24 +329,17 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
<View style={styles.infoHeader}>
<MaterialIcons name="info" size={20} color={colors.primary} />
<Text style={[styles.infoTitle, { color: colors.highEmphasis }]}>
How it works
{t('continue_watching_settings.how_it_works')}
</Text>
</View>
<Text style={[styles.infoText, { color: colors.mediumEmphasis }]}>
{settings.useCachedStreams ? (
<>
Streams are cached for your selected duration after playing{'\n'}
Cached streams are validated before use{'\n'}
If cache is invalid or expired, falls back to content screen{'\n'}
"Use Cached Streams" controls direct player vs screen navigation{'\n'}
"Open Metadata Screen" appears only when cached streams are disabled
{t('continue_watching_settings.how_it_works_cached')}
</>
) : (
<>
When cached streams are disabled, clicking Continue Watching items opens content screens{'\n'}
"Open Metadata Screen" option controls which screen to open{'\n'}
Metadata screen shows content details and allows manual stream selection{'\n'}
Streams screen shows available streams for immediate playback
{t('continue_watching_settings.how_it_works_uncached')}
</>
)}
</Text>
@ -361,7 +358,7 @@ const ContinueWatchingSettingsScreen: React.FC = () => {
]}
>
<MaterialIcons name="check" size={20} color={colors.white} />
<Text style={styles.savedText}>Changes saved</Text>
<Text style={styles.savedText}>{t('continue_watching_settings.changes_saved')}</Text>
</Animated.View>
</SafeAreaView>
);

View file

@ -21,6 +21,7 @@ import { NavigationProp } from '@react-navigation/native';
import FastImage from '@d11/react-native-fast-image';
import { Feather, FontAwesome5 } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
import { useTranslation } from 'react-i18next';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { fetchContributors, GitHubContributor } from '../services/githubReleaseService';
import { RootStackParamList } from '../navigation/AppNavigator';
@ -58,21 +59,21 @@ interface SpecialMention extends SpecialMentionConfig {
isLoading: boolean;
}
const SPECIAL_MENTIONS_CONFIG: SpecialMentionConfig[] = [
const getSpecialMentionsConfig = (t: any) => [
{
discordId: '709281623866081300',
role: 'Community Manager',
description: 'Manages the Discord & Reddit communities for Nuvio',
role: t('contributors.manager_role'),
description: t('contributors.manager_desc'),
},
{
discordId: '777773947071758336',
role: 'Server Sponsor',
description: 'Sponsored the server infrastructure for Nuvio',
role: t('contributors.sponsor_role'),
description: t('contributors.sponsor_desc'),
},
{
discordId: '1395843374241546362',
role: 'Discord Mod',
description: 'Helps moderate the Nuvio Discord community',
role: t('contributors.mod_role'),
description: t('contributors.mod_desc'),
},
];
@ -86,6 +87,7 @@ interface ContributorCardProps {
}
const ContributorCard: React.FC<ContributorCardProps> = ({ contributor, currentTheme, isTablet, isLargeTablet }) => {
const { t } = useTranslation();
const handlePress = useCallback(() => {
Linking.openURL(contributor.html_url);
}, [contributor.html_url]);
@ -121,7 +123,7 @@ const ContributorCard: React.FC<ContributorCardProps> = ({ contributor, currentT
{ color: currentTheme.colors.mediumEmphasis },
isTablet && styles.tabletContributions
]}>
{contributor.contributions} contributions
{contributor.contributions} {t('contributors.contributions')}
</Text>
</View>
<Feather
@ -143,6 +145,7 @@ interface SpecialMentionCardProps {
}
const SpecialMentionCard: React.FC<SpecialMentionCardProps> = ({ mention, currentTheme, isTablet, isLargeTablet }) => {
const { t } = useTranslation();
const handlePress = useCallback(() => {
// Try to open Discord profile
const discordUrl = `discord://-/users/${mention.discordId}`;
@ -153,7 +156,7 @@ const SpecialMentionCard: React.FC<SpecialMentionCardProps> = ({ mention, curren
// Fallback: show alert with Discord info
Alert.alert(
mention.name,
`Discord: @${mention.username}\n\nOpen Discord and search for this user to connect with them.`,
`Discord: @${mention.username}\n\nDo you want to open Discord and search for this user?`,
[{ text: 'OK' }]
);
}
@ -205,7 +208,7 @@ const SpecialMentionCard: React.FC<SpecialMentionCardProps> = ({ mention, curren
{ color: currentTheme.colors.highEmphasis },
isTablet && styles.tabletUsername
]}>
{mention.isLoading ? 'Loading...' : mention.name}
{mention.isLoading ? t('contributors.loading') : mention.name}
</Text>
{!mention.isLoading && mention.username && (
<Text style={[
@ -235,10 +238,13 @@ const SpecialMentionCard: React.FC<SpecialMentionCardProps> = ({ mention, curren
};
const ContributorsScreen: React.FC = () => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const insets = useSafeAreaInsets();
const SPECIAL_MENTIONS_CONFIG = getSpecialMentionsConfig(t);
const [activeTab, setActiveTab] = useState<TabType>('contributors');
const [contributors, setContributors] = useState<GitHubContributor[]>([]);
const [loading, setLoading] = useState(true);
@ -254,7 +260,7 @@ const ContributorsScreen: React.FC = () => {
// Initialize with loading state
const initialMentions: SpecialMention[] = SPECIAL_MENTIONS_CONFIG.map(config => ({
...config,
name: 'Loading...',
name: t('contributors.loading'),
username: '',
avatarUrl: '',
isLoading: true,
@ -283,7 +289,7 @@ const ContributorsScreen: React.FC = () => {
// Return fallback data
return {
...config,
name: 'Discord User',
name: t('contributors.discord_user'),
username: config.discordId,
avatarUrl: '',
isLoading: false,
@ -363,10 +369,10 @@ const ContributorsScreen: React.FC = () => {
await mmkvStorage.removeItem('github_contributors');
await mmkvStorage.removeItem('github_contributors_timestamp');
} catch { }
setError('Unable to load contributors. This might be due to GitHub API rate limits.');
setError(t('contributors.error_rate_limit'));
}
} catch (err) {
setError('Failed to load contributors. Please check your internet connection.');
setError(t('contributors.error_failed'));
if (__DEV__) console.error('Error loading contributors:', err);
} finally {
setLoading(false);
@ -427,7 +433,7 @@ const ContributorsScreen: React.FC = () => {
onPress={() => navigation.goBack()}
>
<Feather name="chevron-left" size={24} color={currentTheme.colors.primary} />
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>{t('common.settings')}</Text>
</TouchableOpacity>
</View>
<Text style={[
@ -435,13 +441,13 @@ const ContributorsScreen: React.FC = () => {
{ color: currentTheme.colors.text },
isTablet && styles.tabletHeaderTitle
]}>
Contributors
{t('contributors.title')}
</Text>
</View>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={[styles.loadingText, { color: currentTheme.colors.mediumEmphasis }]}>
Loading contributors...
{t('contributors.loading_contributors')}
</Text>
</View>
</View>
@ -462,7 +468,7 @@ const ContributorsScreen: React.FC = () => {
onPress={() => navigation.goBack()}
>
<Feather name="chevron-left" size={24} color={currentTheme.colors.primary} />
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>Settings</Text>
<Text style={[styles.backText, { color: currentTheme.colors.primary }]}>{t('common.settings')}</Text>
</TouchableOpacity>
</View>
<Text style={[
@ -470,7 +476,7 @@ const ContributorsScreen: React.FC = () => {
{ color: currentTheme.colors.text },
isTablet && styles.tabletHeaderTitle
]}>
Contributors
{t('contributors.title')}
</Text>
</View>
@ -494,7 +500,7 @@ const ContributorsScreen: React.FC = () => {
{ color: activeTab === 'contributors' ? currentTheme.colors.white : currentTheme.colors.mediumEmphasis },
isTablet && styles.tabletTabText
]}>
Contributors
{t('contributors.tab_contributors')}
</Text>
</TouchableOpacity>
<TouchableOpacity
@ -511,7 +517,7 @@ const ContributorsScreen: React.FC = () => {
{ color: activeTab === 'special' ? currentTheme.colors.white : currentTheme.colors.mediumEmphasis },
isTablet && styles.tabletTabText
]}>
Special Mentions
{t('contributors.tab_special')}
</Text>
</TouchableOpacity>
</View>
@ -528,14 +534,14 @@ const ContributorsScreen: React.FC = () => {
{error}
</Text>
<Text style={[styles.errorSubtext, { color: currentTheme.colors.mediumEmphasis }]}>
GitHub API rate limit exceeded. Please try again later or pull to refresh.
{t('contributors.error_rate_limit')}
</Text>
<TouchableOpacity
style={[styles.retryButton, { backgroundColor: currentTheme.colors.primary }]}
onPress={() => loadContributors()}
>
<Text style={[styles.retryText, { color: currentTheme.colors.white }]}>
Try Again
{t('contributors.retry')}
</Text>
</TouchableOpacity>
</View>
@ -543,7 +549,7 @@ const ContributorsScreen: React.FC = () => {
<View style={styles.emptyContainer}>
<Feather name="users" size={48} color={currentTheme.colors.mediumEmphasis} />
<Text style={[styles.emptyText, { color: currentTheme.colors.mediumEmphasis }]}>
No contributors found
{t('contributors.no_contributors')}
</Text>
</View>
) : (
@ -575,14 +581,14 @@ const ContributorsScreen: React.FC = () => {
{ color: currentTheme.colors.highEmphasis },
isTablet && styles.tabletGratitudeText
]}>
We're grateful for every contribution
{t('contributors.gratitude_title')}
</Text>
<Text style={[
styles.gratitudeSubtext,
{ color: currentTheme.colors.mediumEmphasis },
isTablet && styles.tabletGratitudeSubtext
]}>
Each line of code, bug report, and suggestion helps make Nuvio better for everyone
{t('contributors.gratitude_desc')}
</Text>
</View>
</View>
@ -622,14 +628,14 @@ const ContributorsScreen: React.FC = () => {
{ color: currentTheme.colors.highEmphasis },
isTablet && styles.tabletGratitudeText
]}>
Special Thanks
{t('contributors.special_thanks_title')}
</Text>
<Text style={[
styles.gratitudeSubtext,
{ color: currentTheme.colors.mediumEmphasis },
isTablet && styles.tabletGratitudeSubtext
]}>
These amazing people help keep the Nuvio community running and the servers online
{t('contributors.special_thanks_desc')}
</Text>
</View>
</View>

View file

@ -21,6 +21,7 @@ import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { RootStackParamList } from '../navigation/AppNavigator';
import { useTheme } from '../contexts/ThemeContext';
import { useTranslation } from 'react-i18next';
import { Feather, MaterialCommunityIcons, MaterialIcons } from '@expo/vector-icons';
import { stremioService } from '../services/stremioService';
import { logger } from '../utils/logger';
@ -160,13 +161,13 @@ const DEFAULT_TORRENTIO_CONFIG: TorrentioConfig = {
isInstalled: false,
};
const getPlanName = (plan: number): string => {
const getPlanName = (plan: number, t: any): string => {
switch (plan) {
case 0: return 'Free';
case 1: return 'Essential ($3/mo)';
case 2: return 'Pro ($10/mo)';
case 3: return 'Standard ($5/mo)';
default: return 'Unknown';
case 0: return t('debrid.plan_free');
case 1: return t('debrid.plan_essential');
case 2: return t('debrid.plan_pro');
case 3: return t('debrid.plan_standard');
default: return t('debrid.plan_unknown');
}
};
@ -687,6 +688,7 @@ const createStyles = (colors: any) => StyleSheet.create({
});
const DebridIntegrationScreen = () => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const colors = currentTheme.colors;
@ -831,8 +833,8 @@ const DebridIntegrationScreen = () => {
// Torbox handlers
const handleConnect = async () => {
if (!apiKey.trim()) {
setAlertTitle('Error');
setAlertMessage('Please enter a valid API Key');
setAlertTitle(t('common.error'));
setAlertMessage(t('debrid.error_api_required')); // Reusing key or common error
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
return;
@ -860,8 +862,8 @@ const DebridIntegrationScreen = () => {
setConfig(newConfig);
setApiKey('');
setAlertTitle('Success');
setAlertMessage('Torbox addon connected successfully!');
setAlertTitle(t('common.success'));
setAlertMessage(t('debrid.connected_title')); // Or similar success message
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
@ -888,12 +890,12 @@ const DebridIntegrationScreen = () => {
};
const handleDisconnect = async () => {
setAlertTitle('Disconnect Torbox');
setAlertMessage('Are you sure you want to disconnect Torbox? This will remove the addon and clear your saved API key.');
setAlertTitle(t('debrid.alert_disconnect_title'));
setAlertMessage(t('debrid.alert_disconnect_msg'));
setAlertActions([
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
{
label: 'Disconnect',
label: t('debrid.disconnect_button'),
onPress: async () => {
setAlertVisible(false);
setLoading(true);
@ -1007,8 +1009,8 @@ const DebridIntegrationScreen = () => {
const handleInstallTorrentio = async () => {
// Check if API key is provided
if (!torrentioConfig.debridApiKey.trim()) {
setAlertTitle('API Key Required');
setAlertMessage('Please enter your debrid service API key to install Torrentio.');
setAlertTitle(t('debrid.error_api_required'));
setAlertMessage(t('debrid.error_api_required_desc'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
return;
@ -1042,8 +1044,8 @@ const DebridIntegrationScreen = () => {
await mmkvStorage.setItem(TORRENTIO_CONFIG_KEY, JSON.stringify(newConfig));
setTorrentioConfig(newConfig);
setAlertTitle('Success');
setAlertMessage('Torrentio addon installed successfully!');
setAlertTitle(t('common.success'));
setAlertMessage(t('debrid.success_installed'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
@ -1061,9 +1063,9 @@ const DebridIntegrationScreen = () => {
setAlertTitle('Remove Torrentio');
setAlertMessage('Are you sure you want to remove the Torrentio addon?');
setAlertActions([
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
{ label: t('common.cancel'), onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
{
label: 'Remove',
label: t('debrid.remove_button'),
onPress: async () => {
setAlertVisible(false);
setTorrentioLoading(true);
@ -1087,8 +1089,8 @@ const DebridIntegrationScreen = () => {
await mmkvStorage.setItem(TORRENTIO_CONFIG_KEY, JSON.stringify(newConfig));
setTorrentioConfig(newConfig);
setAlertTitle('Success');
setAlertMessage('Torrentio addon removed successfully');
setAlertTitle(t('common.success'));
setAlertMessage(t('debrid.success_removed'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
@ -1114,14 +1116,14 @@ const DebridIntegrationScreen = () => {
<>
<View style={styles.statusCard}>
<View style={styles.statusRow}>
<Text style={styles.statusLabel}>Status</Text>
<Text style={[styles.statusValue, styles.statusConnected]}>Connected</Text>
<Text style={styles.statusLabel}>{t('common.status')}</Text>
<Text style={[styles.statusValue, styles.statusConnected]}>{t('debrid.status_connected')}</Text>
</View>
<View style={styles.divider} />
<View style={styles.statusRow}>
<Text style={styles.statusLabel}>Enable Addon</Text>
<Text style={styles.statusLabel}>{t('debrid.enable_addon')}</Text>
<Switch
value={config.isEnabled}
onValueChange={handleToggleEnabled}
@ -1138,28 +1140,28 @@ const DebridIntegrationScreen = () => {
disabled={loading}
>
<Text style={styles.buttonText}>
{loading ? 'Disconnecting...' : 'Disconnect & Remove'}
{loading ? t('debrid.disconnect_loading') : t('debrid.disconnect_button')}
</Text>
</TouchableOpacity>
{userData && (
<View style={styles.userDataCard}>
<View style={styles.userDataHeader}>
<Text style={styles.userDataTitle}>Account Information</Text>
<Text style={styles.userDataTitle}>{t('debrid.account_info')}</Text>
{userDataLoading && (
<ActivityIndicator size="small" color={colors.primary} />
)}
</View>
<View style={styles.userDataRow}>
<Text style={styles.userDataLabel}>Email</Text>
<Text style={styles.userDataLabel}>{t('common.email')}</Text>
<Text style={styles.userDataValue} numberOfLines={1}>
{userData.base_email || userData.email}
</Text>
</View>
<View style={styles.userDataRow}>
<Text style={styles.userDataLabel}>Plan</Text>
<Text style={styles.userDataLabel}>{t('debrid.plan')}</Text>
<View style={[
styles.planBadge,
userData.plan === 0 ? styles.planBadgeFree : styles.planBadgePaid
@ -1168,24 +1170,24 @@ const DebridIntegrationScreen = () => {
styles.planBadgeText,
userData.plan === 0 ? styles.planBadgeTextFree : styles.planBadgeTextPaid
]}>
{getPlanName(userData.plan)}
{getPlanName(userData.plan, t)}
</Text>
</View>
</View>
<View style={styles.userDataRow}>
<Text style={styles.userDataLabel}>Status</Text>
<Text style={styles.userDataLabel}>{t('common.status')}</Text>
<Text style={[
styles.userDataValue,
{ color: userData.is_subscribed ? (colors.success || '#4CAF50') : colors.mediumEmphasis }
]}>
{userData.is_subscribed ? 'Active' : 'Free'}
{userData.is_subscribed ? t('debrid.status_active') : t('debrid.plan_free')}
</Text>
</View>
{userData.premium_expires_at && (
<View style={styles.userDataRow}>
<Text style={styles.userDataLabel}>Expires</Text>
<Text style={styles.userDataLabel}>{t('debrid.expires')}</Text>
<Text style={styles.userDataValue}>
{new Date(userData.premium_expires_at).toLocaleDateString()}
</Text>
@ -1193,7 +1195,7 @@ const DebridIntegrationScreen = () => {
)}
<View style={styles.userDataRow}>
<Text style={styles.userDataLabel}>Downloaded</Text>
<Text style={styles.userDataLabel}>{t('debrid.downloaded')}</Text>
<Text style={styles.userDataValue}>
{(userData.total_downloaded / (1024 * 1024 * 1024)).toFixed(2)} GB
</Text>
@ -1202,40 +1204,40 @@ const DebridIntegrationScreen = () => {
)}
<View style={styles.section}>
<Text style={styles.sectionTitle}> Connected to TorBox</Text>
<Text style={styles.sectionTitle}>{t('debrid.connected_title')}</Text>
<Text style={styles.sectionText}>
Your TorBox addon is active and providing premium streams.{config.isEnabled ? '' : ' (Currently disabled)'}
{t('debrid.connected_desc')}
</Text>
</View>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Configure Addon</Text>
<Text style={styles.sectionTitle}>{t('debrid.configure_title')}</Text>
<Text style={styles.sectionText}>
Customize your streaming experience. Sort by quality, filter file sizes, and manage other integration settings.
{t('debrid.configure_desc')}
</Text>
<TouchableOpacity
style={styles.subscribeButton}
onPress={() => Linking.openURL('https://torbox.app/settings?section=integration-settings')}
>
<Text style={styles.subscribeButtonText}>Open Settings</Text>
<Text style={styles.subscribeButtonText}>{t('debrid.open_settings')}</Text>
</TouchableOpacity>
</View>
</>
) : (
<>
<Text style={styles.description}>
Unlock 4K high-quality streams and lightning-fast speeds by integrating Torbox. Enter your API Key below to instantly upgrade your streaming experience.
{t('debrid.description_torbox')}
</Text>
<TouchableOpacity onPress={() => Linking.openURL('https://guides.viren070.me/stremio/technical-details#debrid-services')} style={styles.guideLink}>
<Text style={styles.guideLinkText}>What is a Debrid Service?</Text>
<Text style={styles.guideLinkText}>{t('debrid.what_is_debrid')}</Text>
</TouchableOpacity>
<View style={styles.inputContainer}>
<Text style={styles.label}>Torbox API Key</Text>
<Text style={styles.label}>{t('debrid.api_key_label')}</Text>
<TextInput
style={styles.input}
placeholder="Enter your API Key"
placeholder={t('debrid.enter_api_key')}
placeholderTextColor={colors.mediumGray}
value={apiKey}
onChangeText={setApiKey}
@ -1251,24 +1253,24 @@ const DebridIntegrationScreen = () => {
disabled={loading}
>
<Text style={styles.connectButtonText}>
{loading ? 'Connecting...' : 'Connect & Install'}
{loading ? t('debrid.connecting') : t('debrid.connect_button')}
</Text>
</TouchableOpacity>
<View style={styles.section}>
<Text style={styles.sectionTitle}>Unlock Premium Speeds</Text>
<Text style={styles.sectionTitle}>{t('debrid.unlock_speeds_title')}</Text>
<Text style={styles.sectionText}>
Get a Torbox subscription to access cached high-quality streams with zero buffering.
{t('debrid.unlock_speeds_desc')}
</Text>
<TouchableOpacity style={styles.subscribeButton} onPress={openSubscription}>
<Text style={styles.subscribeButtonText}>Get Subscription</Text>
<Text style={styles.subscribeButtonText}>{t('debrid.get_subscription')}</Text>
</TouchableOpacity>
</View>
</>
)}
<View style={[styles.logoContainer, { marginTop: 60 }]}>
<Text style={styles.poweredBy}>Powered by</Text>
<Text style={styles.poweredBy}>{t('debrid.powered_by')}</Text>
<View style={styles.logoRow}>
<Image
source={{ uri: 'https://torbox.app/assets/logo-bb7a9579.svg' }}
@ -1277,7 +1279,7 @@ const DebridIntegrationScreen = () => {
/>
<Text style={styles.logoText}>TorBox</Text>
</View>
<Text style={styles.disclaimer}>Nuvio is not affiliated with Torbox in any way.</Text>
<Text style={styles.disclaimer}>{t('debrid.disclaimer_torbox')}</Text>
</View>
</>
);
@ -1290,34 +1292,34 @@ const DebridIntegrationScreen = () => {
const renderTorrentioTab = () => (
<>
<Text style={styles.description}>
Configure Torrentio to get torrent streams for movies and TV shows. A debrid service is required to stream content.
{t('debrid.description_torrentio')}
</Text>
{torrentioConfig.isInstalled && (
<View style={styles.installedBadge}>
<Text style={styles.installedBadgeText}> INSTALLED</Text>
<Text style={styles.installedBadgeText}>{t('debrid.installed_badge')}</Text>
</View>
)}
{/* TorBox Promotion Card */}
{!torrentioConfig.debridApiKey && (
<View style={styles.promoCard}>
<Text style={styles.promoTitle}> Need a Debrid Service?</Text>
<Text style={styles.promoTitle}>{t('debrid.promo_title')}</Text>
<Text style={styles.promoText}>
Get TorBox for lightning-fast 4K streaming with zero buffering. Premium cached torrents and instant downloads.
{t('debrid.promo_desc')}
</Text>
<TouchableOpacity
style={styles.promoButton}
onPress={() => Linking.openURL('https://torbox.app/subscription?referral=493192f2-6403-440f-b414-768f72222ec7')}
>
<Text style={styles.promoButtonText}>Get TorBox Subscription</Text>
<Text style={styles.promoButtonText}>{t('debrid.promo_button')}</Text>
</TouchableOpacity>
</View>
)}
{/* Debrid Service Selection */}
<View style={styles.configSection}>
<Text style={styles.configSectionTitle}>Debrid Service *</Text>
<Text style={styles.configSectionTitle}>{t('debrid.service_label')}</Text>
<View style={styles.pickerContainer}>
{TORRENTIO_DEBRID_SERVICES.map((service: any) => (
<TouchableOpacity
@ -1341,7 +1343,7 @@ const DebridIntegrationScreen = () => {
{/* Debrid API Key */}
<View style={styles.configSection}>
<Text style={styles.configSectionTitle}>API Key *</Text>
<Text style={styles.configSectionTitle}>{t('debrid.api_key_label')}</Text>
<TextInput
style={styles.input}
placeholder={`Enter your ${TORRENTIO_DEBRID_SERVICES.find((d: any) => d.id === torrentioConfig.debridService)?.name || 'Debrid'} API Key`}
@ -1360,7 +1362,7 @@ const DebridIntegrationScreen = () => {
onPress={() => toggleSection('sorting')}
>
<View>
<Text style={styles.accordionHeaderText}>Sorting</Text>
<Text style={styles.accordionHeaderText}>{t('debrid.sorting_label')}</Text>
<Text style={styles.accordionSubtext}>
{TORRENTIO_SORT_OPTIONS.find(o => o.id === torrentioConfig.sort)?.name || 'By quality'}
</Text>
@ -1391,7 +1393,7 @@ const DebridIntegrationScreen = () => {
onPress={() => toggleSection('qualityFilter')}
>
<View>
<Text style={styles.accordionHeaderText}>Exclude Qualities</Text>
<Text style={styles.accordionHeaderText}>{t('debrid.exclude_qualities')}</Text>
<Text style={styles.accordionSubtext}>
{torrentioConfig.qualityFilter.length > 0 ? `${torrentioConfig.qualityFilter.length} excluded` : 'None excluded'}
</Text>
@ -1422,9 +1424,9 @@ const DebridIntegrationScreen = () => {
onPress={() => toggleSection('languages')}
>
<View>
<Text style={styles.accordionHeaderText}>Priority Languages</Text>
<Text style={styles.accordionHeaderText}>{t('debrid.priority_languages')}</Text>
<Text style={styles.accordionSubtext}>
{torrentioConfig.priorityLanguages.length > 0 ? `${torrentioConfig.priorityLanguages.length} selected` : 'No preference'}
{torrentioConfig.priorityLanguages.length > 0 ? `${torrentioConfig.priorityLanguages.length} ${t('home_screen.selected')}` : 'No preference'}
</Text>
</View>
<Feather name={expandedSections.languages ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
@ -1453,7 +1455,7 @@ const DebridIntegrationScreen = () => {
onPress={() => toggleSection('maxResults')}
>
<View>
<Text style={styles.accordionHeaderText}>Max Results</Text>
<Text style={styles.accordionHeaderText}>{t('debrid.max_results')}</Text>
<Text style={styles.accordionSubtext}>
{TORRENTIO_MAX_RESULTS.find(o => o.id === torrentioConfig.maxResults)?.name || 'All results'}
</Text>
@ -1484,7 +1486,7 @@ const DebridIntegrationScreen = () => {
onPress={() => toggleSection('options')}
>
<View>
<Text style={styles.accordionHeaderText}>Additional Options</Text>
<Text style={styles.accordionHeaderText}>{t('debrid.additional_options')}</Text>
<Text style={styles.accordionSubtext}>Catalog & download settings</Text>
</View>
<Feather name={expandedSections.options ? 'chevron-up' : 'chevron-down'} size={20} color={colors.mediumEmphasis} />
@ -1492,7 +1494,7 @@ const DebridIntegrationScreen = () => {
{expandedSections.options && (
<View style={styles.accordionContent}>
<View style={styles.switchRow}>
<Text style={styles.switchLabel}>Don't show download links</Text>
<Text style={styles.switchLabel}>{t('debrid.no_download_links')}</Text>
<Switch
value={torrentioConfig.noDownloadLinks}
onValueChange={(val) => setTorrentioConfig(prev => ({ ...prev, noDownloadLinks: val }))}
@ -1501,7 +1503,7 @@ const DebridIntegrationScreen = () => {
/>
</View>
<View style={styles.switchRow}>
<Text style={styles.switchLabel}>Don't show debrid catalog</Text>
<Text style={styles.switchLabel}>{t('debrid.no_debrid_catalog')}</Text>
<Switch
value={torrentioConfig.noCatalog}
onValueChange={(val) => setTorrentioConfig(prev => ({ ...prev, noCatalog: val }))}
@ -1532,7 +1534,7 @@ const DebridIntegrationScreen = () => {
disabled={torrentioLoading}
>
<Text style={styles.connectButtonText}>
{torrentioLoading ? 'Updating...' : 'Update Configuration'}
{torrentioLoading ? t('debrid.updating') : t('debrid.update_button')}
</Text>
</TouchableOpacity>
<TouchableOpacity
@ -1540,7 +1542,7 @@ const DebridIntegrationScreen = () => {
onPress={handleRemoveTorrentio}
disabled={torrentioLoading}
>
<Text style={styles.buttonText}>Remove Torrentio</Text>
<Text style={styles.buttonText}>{t('debrid.remove_button')}</Text>
</TouchableOpacity>
</>
) : (
@ -1550,14 +1552,14 @@ const DebridIntegrationScreen = () => {
disabled={torrentioLoading}
>
<Text style={styles.connectButtonText}>
{torrentioLoading ? 'Installing...' : 'Install Torrentio'}
{torrentioLoading ? t('debrid.installing') : t('debrid.install_button')}
</Text>
</TouchableOpacity>
)}
</View>
<Text style={[styles.disclaimer, { marginTop: 24, marginBottom: 40 }]}>
Nuvio is not affiliated with Torrentio in any way.
{t('debrid.disclaimer_torrentio')}
</Text>
</>
);
@ -1584,7 +1586,7 @@ const DebridIntegrationScreen = () => {
>
<Feather name="arrow-left" size={24} color={colors.white} />
</TouchableOpacity>
<Text style={styles.headerTitle}>Debrid Integration</Text>
<Text style={styles.headerTitle}>{t('debrid.title')}</Text>
</View>
{/* Tab Selector */}
@ -1594,7 +1596,7 @@ const DebridIntegrationScreen = () => {
onPress={() => setActiveTab('torbox')}
>
<Text style={[styles.tabText, activeTab === 'torbox' && styles.activeTabText]}>
TorBox
{t('debrid.tab_torbox')}
</Text>
</TouchableOpacity>
<TouchableOpacity
@ -1602,7 +1604,7 @@ const DebridIntegrationScreen = () => {
onPress={() => setActiveTab('torrentio')}
>
<Text style={[styles.tabText, activeTab === 'torrentio' && styles.activeTabText]}>
Torrentio
{t('debrid.tab_torrentio')}
</Text>
</TouchableOpacity>
</View>

View file

@ -14,9 +14,11 @@ import {
Dimensions
} from 'react-native';
import { useSettings } from '../hooks/useSettings';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
import { useNavigation, useFocusEffect } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { MaterialIcons } from '@expo/vector-icons';
import { MaterialIcons, Feather } from '@expo/vector-icons';
import { useTheme } from '../contexts/ThemeContext';
import { RootStackParamList } from '../navigation/AppNavigator';
@ -107,6 +109,7 @@ const SectionHeader: React.FC<{ title: string; isDarkMode: boolean; colors: any
);
const HomeScreenSettings: React.FC = () => {
const { t } = useTranslation();
const { settings, updateSetting } = useSettings();
const systemColorScheme = useColorScheme();
const { currentTheme } = useTheme();
@ -247,11 +250,11 @@ const HomeScreenSettings: React.FC = () => {
// Format selected catalogs text
const getSelectedCatalogsText = useCallback(() => {
if (!settings.selectedHeroCatalogs || settings.selectedHeroCatalogs.length === 0) {
return "All catalogs";
return t("home_screen.all_catalogs");
} else {
return `${settings.selectedHeroCatalogs.length} selected`;
return `${settings.selectedHeroCatalogs.length} ${t("home_screen.selected")}`;
}
}, [settings.selectedHeroCatalogs]);
}, [settings.selectedHeroCatalogs, t]);
const ChevronRight = () => (
<MaterialIcons
@ -268,14 +271,10 @@ const HomeScreenSettings: React.FC = () => {
]}>
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}>
<TouchableOpacity onPress={handleBack} style={styles.backButton}>
<MaterialIcons
name="arrow-back"
size={24}
color={isDarkMode ? colors.highEmphasis : colors.textDark}
/>
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton}>
<Feather name="arrow-left" size={24} color={currentTheme.colors.text} />
<Text style={[styles.backText, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
Settings
{t('settings.title')}
</Text>
</TouchableOpacity>
@ -285,7 +284,7 @@ const HomeScreenSettings: React.FC = () => {
</View>
<Text style={[styles.headerTitle, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>
Home Screen Settings
{t('home_screen.title')}
</Text>
{/* Saved indicator */}
@ -300,7 +299,7 @@ const HomeScreenSettings: React.FC = () => {
pointerEvents="none"
>
<MaterialIcons name="check-circle" size={20} color="#FFFFFF" />
<Text style={styles.savedIndicatorText}>Changes Applied</Text>
<Text style={styles.savedIndicatorText}>{t('home_screen.changes_applied')}</Text>
</Animated.View>
<ScrollView
@ -308,11 +307,11 @@ const HomeScreenSettings: React.FC = () => {
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
<SectionHeader title="DISPLAY OPTIONS" isDarkMode={isDarkMode} colors={colors} />
<SectionHeader title={t("home_screen.display_options")} isDarkMode={isDarkMode} colors={colors} />
<SettingsCard isDarkMode={isDarkMode} colors={colors}>
<SettingItem
title="Show Hero Section"
description="Featured content at the top"
title={t("home_screen.show_hero")}
description={t("home_screen.show_hero_desc")}
icon="movie-filter"
isDarkMode={isDarkMode}
colors={colors}
@ -324,8 +323,8 @@ const HomeScreenSettings: React.FC = () => {
)}
/>
<SettingItem
title="Show This Week Section"
description="New episodes from current week"
title={t("home_screen.show_this_week")}
description={t("home_screen.show_this_week_desc")}
icon="date-range"
isDarkMode={isDarkMode}
colors={colors}
@ -338,7 +337,7 @@ const HomeScreenSettings: React.FC = () => {
/>
{settings.showHeroSection && (
<SettingItem
title="Select Catalogs"
title={t("home_screen.select_catalogs")}
description={getSelectedCatalogsText()}
icon="list"
isDarkMode={isDarkMode}
@ -354,29 +353,29 @@ const HomeScreenSettings: React.FC = () => {
<>
{!isTabletDevice && (
<View style={styles.segmentCard}>
<Text style={[styles.segmentTitle, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>Hero Layout</Text>
<Text style={[styles.segmentTitle, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>{t('home_screen.hero_layout')}</Text>
<SegmentedControl
options={[
{ label: 'Legacy', value: 'legacy' },
{ label: 'Carousel', value: 'carousel' },
{ label: 'Apple TV', value: 'appletv' }
{ label: t('home_screen.layout_legacy'), value: 'legacy' },
{ label: t('home_screen.layout_carousel'), value: 'carousel' },
{ label: t('home_screen.layout_appletv'), value: 'appletv' }
]}
value={settings.heroStyle}
onChange={(val) => handleUpdateSetting('heroStyle', val as any)}
/>
<Text style={[styles.segmentHint, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>Full-width banner, swipeable cards, or Apple TV style</Text>
<Text style={[styles.segmentHint, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>{t('home_screen.layout_desc')}</Text>
</View>
)}
<View style={styles.segmentCard}>
<Text style={[styles.segmentTitle, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>Featured Source</Text>
<Text style={[styles.segmentHint, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>Using Catalogs</Text>
<Text style={[styles.segmentTitle, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>{t('home_screen.featured_source')}</Text>
<Text style={[styles.segmentHint, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>{t('home_screen.using_catalogs')}</Text>
<TouchableOpacity
onPress={() => navigation.navigate('HeroCatalogs')}
style={[styles.manageLink, { backgroundColor: isDarkMode ? colors.elevation1 : 'rgba(0,0,0,0.04)' }]}
activeOpacity={0.8}
>
<Text style={{ color: isDarkMode ? colors.highEmphasis : colors.textDark, fontWeight: '600' }}>Manage selected catalogs</Text>
<Text style={{ color: isDarkMode ? colors.highEmphasis : colors.textDark, fontWeight: '600' }}>{t('home_screen.manage_selected_catalogs')}</Text>
<MaterialIcons name="chevron-right" size={20} color={isDarkMode ? colors.mediumEmphasis : colors.textMutedDark} />
</TouchableOpacity>
</View>
@ -384,8 +383,8 @@ const HomeScreenSettings: React.FC = () => {
{settings.heroStyle === 'carousel' && (
<SettingsCard isDarkMode={isDarkMode} colors={colors}>
<SettingItem
title="Dynamic Hero Background"
description="Blurred banner behind carousel"
title={t("home_screen.dynamic_hero_background")}
description={t("home_screen.dynamic_hero_background_desc")}
icon="wallpaper"
isDarkMode={isDarkMode}
colors={colors}
@ -396,44 +395,44 @@ const HomeScreenSettings: React.FC = () => {
/>
)}
/>
<Text style={[styles.settingInlineNote, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>May impact performance on low-end devices.</Text>
<Text style={[styles.settingInlineNote, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>{t('home_screen.performance_note')}</Text>
</SettingsCard>
)}
</>
)}
<SettingsCard isDarkMode={isDarkMode} colors={colors}>
<Text style={[styles.cardHeader, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>Posters</Text>
<Text style={[styles.cardHeader, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>{t('home_screen.posters')}</Text>
<View style={styles.settingsRowInline}>
<Text style={[styles.rowLabel, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>Show Titles</Text>
<Text style={[styles.rowLabel, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>{t('home_screen.show_titles')}</Text>
<CustomSwitch
value={settings.showPosterTitles}
onValueChange={(value) => handleUpdateSetting('showPosterTitles', value)}
/>
</View>
<View style={styles.settingsRow}>
<Text style={[styles.rowLabel, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>Poster Size</Text>
<Text style={[styles.rowLabel, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>{t('home_screen.poster_size')}</Text>
<SegmentedControl
options={[{ label: 'Small', value: 'small' }, { label: 'Medium', value: 'medium' }, { label: 'Large', value: 'large' }]}
options={[{ label: t('home_screen.size_small'), value: 'small' }, { label: t('home_screen.size_medium'), value: 'medium' }, { label: t('home_screen.size_large'), value: 'large' }]}
value={settings.posterSize}
onChange={(val) => handleUpdateSetting('posterSize', val as any)}
/>
</View>
<View style={styles.settingsRow}>
<Text style={[styles.rowLabel, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>Poster Corners</Text>
<Text style={[styles.rowLabel, { color: isDarkMode ? colors.highEmphasis : colors.textDark }]}>{t('home_screen.poster_corners')}</Text>
<SegmentedControl
options={[{ label: 'Square', value: '0' }, { label: 'Rounded', value: '12' }, { label: 'Pill', value: '20' }]}
options={[{ label: t('home_screen.corners_square'), value: '0' }, { label: t('home_screen.corners_rounded'), value: '12' }, { label: t('home_screen.corners_pill'), value: '20' }]}
value={String(settings.posterBorderRadius)}
onChange={(val) => handleUpdateSetting('posterBorderRadius', Number(val) as any)}
/>
</View>
</SettingsCard>
<SectionHeader title="ABOUT THESE SETTINGS" isDarkMode={isDarkMode} colors={colors} />
<SectionHeader title={t("home_screen.about_these_settings")} isDarkMode={isDarkMode} colors={colors} />
<View style={[styles.infoCard, { backgroundColor: isDarkMode ? colors.elevation1 : 'rgba(0,0,0,0.03)' }]}>
<Text style={[styles.infoText, { color: isDarkMode ? colors.mediumEmphasis : colors.textMutedDark }]}>
These settings control how content is displayed on your Home screen. Changes are applied immediately without requiring an app restart.
{t('home_screen.about_desc')}
</Text>
</View>
</ScrollView>

View file

@ -14,14 +14,18 @@ import {
Keyboard,
Clipboard,
Switch,
useColorScheme,
} from 'react-native';
import CustomAlert from '../components/CustomAlert';
import { useNavigation } from '@react-navigation/native';
import { LinearGradient } from 'expo-linear-gradient';
import { useTranslation } from 'react-i18next';
import { useNavigation, useFocusEffect, NavigationProp } from '@react-navigation/native';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import Feather from 'react-native-vector-icons/Feather'; // Added Feather icon import
import { mmkvStorage } from '../services/mmkvStorage';
import { useTheme } from '../contexts/ThemeContext';
import { logger } from '../utils/logger';
import { RATING_PROVIDERS } from '../components/metadata/RatingsSection';
import CustomAlert from '../components/CustomAlert'; // Moved CustomAlert import here
export const MDBLIST_API_KEY_STORAGE_KEY = 'mdblist_api_key';
export const RATING_PROVIDERS_STORAGE_KEY = 'rating_providers_config';
@ -47,7 +51,7 @@ export const getMDBListAPIKey = async (): Promise<string | null> => {
logger.log('[MDBList] MDBList is disabled, not retrieving API key');
return null;
}
return await mmkvStorage.getItem(MDBLIST_API_KEY_STORAGE_KEY);
} catch (error) {
logger.error('[MDBList] Error retrieving API key:', error);
@ -64,9 +68,9 @@ const createStyles = (colors: any) => StyleSheet.create({
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 16,
paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8,
paddingBottom: 10,
},
backButton: {
flexDirection: 'row',
@ -87,6 +91,11 @@ const createStyles = (colors: any) => StyleSheet.create({
paddingBottom: 16,
paddingTop: 8,
},
title: {
fontSize: 20,
fontWeight: '600',
marginLeft: 10,
},
content: {
flex: 1,
},
@ -134,12 +143,20 @@ const createStyles = (colors: any) => StyleSheet.create({
statusTextContainer: {
flex: 1,
},
statusContent: {
flex: 1,
},
statusTitle: {
fontSize: 16,
fontWeight: '600',
color: colors.white,
marginBottom: 2,
},
statusSubtitle: {
fontSize: 13,
color: colors.mediumGray,
lineHeight: 18,
},
statusDescription: {
fontSize: 13,
color: colors.mediumGray,
@ -151,6 +168,11 @@ const createStyles = (colors: any) => StyleSheet.create({
color: colors.lightGray,
marginBottom: 10,
},
sectionSubtitle: {
fontSize: 13,
color: colors.mediumGray,
marginBottom: 12,
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
@ -159,6 +181,17 @@ const createStyles = (colors: any) => StyleSheet.create({
borderWidth: 1,
borderColor: colors.border,
},
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: 8,
borderWidth: 1,
paddingHorizontal: 10,
marginBottom: 10,
},
inputIcon: {
marginRight: 10,
},
input: {
flex: 1,
paddingVertical: 10,
@ -197,7 +230,7 @@ const createStyles = (colors: any) => StyleSheet.create({
},
buttonContainer: {
marginTop: 12,
gap: 10,
gap: 10,
},
buttonIcon: {
marginRight: 6,
@ -212,7 +245,7 @@ const createStyles = (colors: any) => StyleSheet.create({
justifyContent: 'center',
},
saveButtonDisabled: {
backgroundColor: colors.elevation2,
backgroundColor: colors.elevation2,
opacity: 0.8,
},
saveButtonText: {
@ -242,12 +275,15 @@ const createStyles = (colors: any) => StyleSheet.create({
clearButtonTextDisabled: {
color: colors.darkGray,
},
buttonDisabled: {
opacity: 0.5,
},
infoHeader: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 10,
},
infoHeaderText: {
infoTitle: {
fontSize: 15,
fontWeight: '600',
color: colors.white,
@ -255,7 +291,38 @@ const createStyles = (colors: any) => StyleSheet.create({
},
infoSteps: {
marginBottom: 12,
gap: 6,
gap: 6,
},
stepsContainer: {
marginBottom: 15,
},
stepRow: {
flexDirection: 'row',
alignItems: 'flex-start',
marginBottom: 10,
},
stepNumber: {
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: colors.primary,
justifyContent: 'center',
alignItems: 'center',
marginRight: 10,
},
stepNumberText: {
color: colors.white,
fontSize: 12,
fontWeight: 'bold',
},
stepText: {
flex: 1,
fontSize: 13,
color: colors.mediumGray,
lineHeight: 18,
},
linkText: {
fontWeight: '600',
},
infoStep: {
flexDirection: 'row',
@ -355,12 +422,19 @@ const createStyles = (colors: any) => StyleSheet.create({
},
});
const MDBListSettingsScreen = () => {
const navigation = useNavigation();
interface RootStackParamList {
Settings: undefined;
// Add other routes if necessary
}
const MDBListSettingsScreen: React.FC = () => {
const { t } = useTranslation();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
const isDarkMode = useColorScheme() === 'dark';
const colors = currentTheme.colors;
const styles = createStyles(colors);
// Custom alert state
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
@ -471,38 +545,48 @@ const MDBListSettingsScreen = () => {
const saveApiKey = async () => {
logger.log('[MDBListSettingsScreen] Starting API key save');
Keyboard.dismiss();
try {
const trimmedKey = apiKey.trim();
if (!trimmedKey) {
logger.warn('[MDBListSettingsScreen] Empty API key provided');
setTestResult({ success: false, message: 'API Key cannot be empty.' });
setAlertTitle(t('common.error'));
setAlertMessage(t('mdblist.api_key_empty_error'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
return;
}
logger.log('[MDBListSettingsScreen] Saving API key');
await mmkvStorage.setItem(MDBLIST_API_KEY_STORAGE_KEY, trimmedKey);
setIsKeySet(true);
setTestResult({ success: true, message: 'API key saved successfully.' });
setAlertTitle(t('common.success'));
setAlertMessage(t('mdblist.success_saved'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
logger.log('[MDBListSettingsScreen] API key saved successfully');
} catch (error) {
logger.error('[MDBListSettingsScreen] Error saving API key:', error);
setTestResult({
success: false,
message: 'An error occurred while saving. Please try again.'
});
setAlertTitle(t('common.error'));
setAlertMessage(t('mdblist.error_save'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
};
const clearApiKey = async () => {
const handleClear = async () => {
logger.log('[MDBListSettingsScreen] Clear API key requested');
setAlertTitle('Clear API Key');
setAlertMessage('Are you sure you want to remove the saved API key?');
setAlertTitle(t('mdblist.alert_clear_title'));
setAlertMessage(t('mdblist.alert_clear_msg'));
setAlertActions([
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: colors.mediumGray } },
{
label: 'Clear',
label: t('common.cancel'),
onPress: () => setAlertVisible(false),
style: { color: currentTheme.colors.mediumGray }
},
{
label: t('mdblist.clear'),
onPress: async () => {
logger.log('[MDBListSettingsScreen] Proceeding with API key clear');
try {
@ -510,11 +594,15 @@ const MDBListSettingsScreen = () => {
setApiKey('');
setIsKeySet(false);
setTestResult(null);
setAlertTitle(t('common.success'));
setAlertMessage(t('mdblist.success_cleared'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
logger.log('[MDBListSettingsScreen] API key cleared successfully');
} catch (error) {
logger.error('[MDBListSettingsScreen] Failed to clear API key:', error);
setAlertTitle('Error');
setAlertMessage('Failed to clear API key');
setAlertTitle(t('common.error'));
setAlertMessage(t('mdblist.error_clear'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
@ -554,7 +642,7 @@ const MDBListSettingsScreen = () => {
<StatusBar barStyle="light-content" />
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
<Text style={styles.loadingText}>Loading Settings...</Text>
<Text style={styles.loadingText}>{t('common.loading_settings')}</Text>
</View>
</SafeAreaView>
);
@ -562,44 +650,38 @@ const MDBListSettingsScreen = () => {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="light-content" />
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'} />
<View style={styles.header}>
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<MaterialIcons name="chevron-left" size={28} color={colors.primary} />
<Text style={styles.backText}>Settings</Text>
<TouchableOpacity onPress={() => navigation.goBack()} style={styles.backButton}>
<Feather name="arrow-left" size={24} color={currentTheme.colors.text} />
</TouchableOpacity>
<Text style={[styles.title, { color: currentTheme.colors.text }]}>
{t('mdblist.title')}
</Text>
</View>
<Text style={styles.headerTitle}>Rating Sources</Text>
<ScrollView
<ScrollView
style={styles.content}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
<View style={styles.statusCard}>
<MaterialIcons
name={isKeySet && isMdbListEnabled ? "check-circle" : "error-outline"}
<MaterialIcons
name={isKeySet && isMdbListEnabled ? "check-circle" : "error-outline"}
size={28}
color={isKeySet && isMdbListEnabled ? colors.success : colors.warning}
color={isKeySet && isMdbListEnabled ? colors.success : colors.warning}
style={styles.statusIcon}
/>
<View style={styles.statusTextContainer}>
<Text style={styles.statusTitle}>
{!isMdbListEnabled
? "MDBList Disabled"
: isKeySet
? "API Key Active"
: "API Key Required"}
</Text>
<Text style={styles.statusDescription}>
<View style={styles.statusContent}>
<Text style={[styles.statusTitle, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
{!isMdbListEnabled
? "MDBList functionality is currently disabled."
: isKeySet
? "Ratings from MDBList are enabled."
: "Add your key below to enable ratings."}
? t('mdblist.status_disabled')
: (isKeySet ? t('mdblist.status_active') : t('mdblist.status_required'))}
</Text>
<Text style={[styles.statusSubtitle, { color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }]}>
{!isMdbListEnabled
? t('mdblist.status_disabled_desc')
: (isKeySet ? t('mdblist.status_active_desc') : t('mdblist.status_required_desc'))}
</Text>
</View>
</View>
@ -607,10 +689,8 @@ const MDBListSettingsScreen = () => {
<View style={styles.card}>
<View style={styles.masterToggleContainer}>
<View style={styles.masterToggleInfo}>
<Text style={styles.masterToggleTitle}>Enable MDBList</Text>
<Text style={styles.masterToggleDescription}>
Turn on/off all MDBList functionality
</Text>
<Text style={styles.masterToggleTitle}>{t('mdblist.enable_toggle')}</Text>
<Text style={styles.masterToggleDescription}>{t('mdblist.enable_toggle_desc')}</Text>
</View>
<Switch
value={isMdbListEnabled}
@ -622,21 +702,29 @@ const MDBListSettingsScreen = () => {
</View>
<View style={[styles.card, !isMdbListEnabled && styles.disabledCard]}>
<Text style={styles.sectionTitle}>API Key</Text>
<View style={[styles.inputWrapper, !isMdbListEnabled && styles.disabledInput]}>
<Text style={[styles.sectionTitle, { color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }]}>
{t('mdblist.api_section')}
</Text>
<View style={[styles.inputContainer, {
backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : '#F5F5F5',
borderColor: isDarkMode ? 'transparent' : '#E0E0E0'
}]}>
<MaterialIcons name="vpn-key" size={20} color={currentTheme.colors.mediumEmphasis} style={styles.inputIcon} />
<TextInput
ref={apiKeyInputRef}
style={[
styles.input,
styles.input,
isInputFocused && styles.inputFocused,
!isMdbListEnabled && styles.disabledText
!isMdbListEnabled && styles.disabledText,
{ color: currentTheme.colors.text }
]}
value={apiKey}
onChangeText={(text) => {
setApiKey(text);
if (testResult) setTestResult(null);
}}
placeholder="Paste your MDBList API key"
placeholder={t('mdblist.placeholder')}
placeholderTextColor={!isMdbListEnabled ? colors.darkGray : colors.mediumGray}
autoCapitalize="none"
autoCorrect={false}
@ -644,66 +732,67 @@ const MDBListSettingsScreen = () => {
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
editable={isMdbListEnabled}
secureTextEntry
/>
<TouchableOpacity
style={styles.pasteButton}
<TouchableOpacity
style={styles.pasteButton}
onPress={pasteFromClipboard}
disabled={!isMdbListEnabled}
>
<MaterialIcons
name="content-paste"
size={20}
color={!isMdbListEnabled ? colors.darkGray : colors.primary}
/>
<MaterialIcons
name="content-paste"
size={20}
color={!isMdbListEnabled ? colors.darkGray : colors.primary}
/>
</TouchableOpacity>
</View>
{testResult && (
<View style={[
styles.testResultContainer,
testResult.success ? styles.testResultSuccess : styles.testResultError
]}>
<MaterialIcons
name={testResult.success ? "check" : "warning"}
<MaterialIcons
name={testResult.success ? "check" : "warning"}
size={18}
color={testResult.success ? colors.success : colors.error}
color={testResult.success ? colors.success : colors.error}
/>
<Text style={styles.testResultText}>
{testResult.message}
</Text>
</View>
)}
<View style={styles.buttonContainer}>
<TouchableOpacity
style={[
styles.saveButton,
styles.saveButton,
(!apiKey.trim() || !isMdbListEnabled) && styles.saveButtonDisabled
]}
onPress={saveApiKey}
disabled={!apiKey.trim() || !isMdbListEnabled}
>
<MaterialIcons name="save" size={18} color={colors.white} style={styles.buttonIcon} />
<Text style={styles.saveButtonText}>Save</Text>
<Text style={styles.saveButtonText}>{t('mdblist.save')}</Text>
</TouchableOpacity>
{isKeySet && (
<TouchableOpacity
style={[styles.clearButton, !isMdbListEnabled && styles.clearButtonDisabled]}
onPress={clearApiKey}
onPress={handleClear}
disabled={!isMdbListEnabled}
>
<MaterialIcons
name="delete-outline"
size={18}
color={!isMdbListEnabled ? colors.darkGray : colors.error}
style={styles.buttonIcon}
<MaterialIcons
name="delete-outline"
size={18}
color={!isMdbListEnabled ? colors.darkGray : colors.error}
style={styles.buttonIcon}
/>
<Text style={[
styles.clearButtonText,
styles.clearButtonText,
!isMdbListEnabled && styles.clearButtonTextDisabled
]}>
Clear Key
{t('mdblist.clear')}
</Text>
</TouchableOpacity>
)}
@ -711,9 +800,11 @@ const MDBListSettingsScreen = () => {
</View>
<View style={[styles.card, !isMdbListEnabled && styles.disabledCard]}>
<Text style={styles.sectionTitle}>Rating Providers</Text>
<Text style={styles.sectionDescription}>
Choose which ratings to display in the app
<Text style={[styles.sectionTitle, { color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }]}>
{t('mdblist.rating_providers')}
</Text>
<Text style={[styles.sectionSubtitle, { color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }]}>
{t('mdblist.rating_providers_desc')}
</Text>
{Object.entries(RATING_PROVIDERS).map(([id, provider]) => (
<View key={id} style={styles.providerItem}>
@ -738,68 +829,37 @@ const MDBListSettingsScreen = () => {
<View style={[styles.infoCard, !isMdbListEnabled && styles.disabledCard]}>
<View style={styles.infoHeader}>
<MaterialIcons
name="help-outline"
size={20}
color={!isMdbListEnabled ? colors.darkGray : colors.primary}
/>
<Text style={[
styles.infoHeaderText,
!isMdbListEnabled && styles.disabledText
]}>
How to get an API key
<Feather name="info" size={20} color={currentTheme.colors.primary} />
<Text style={[styles.infoTitle, { color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }]}>
{t('mdblist.how_to')}
</Text>
</View>
<View style={styles.infoSteps}>
<View style={styles.infoStep}>
<Text style={[
styles.infoStepNumber,
!isMdbListEnabled && styles.disabledText
]}>
1.
<View style={styles.stepsContainer}>
<View style={styles.stepRow}>
<View style={[styles.stepNumber, { backgroundColor: currentTheme.colors.primary }]}>
<Text style={styles.stepNumberText}>1</Text>
</View>
<Text style={[styles.stepText, { color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }]}>
{t('mdblist.step_1')} <Text style={[styles.linkText, { color: currentTheme.colors.primary }]} onPress={openMDBListWebsite}>{t('mdblist.step_1_link')}</Text>.
</Text>
<Text style={[
styles.infoStepText,
!isMdbListEnabled && styles.disabledText
]}>
Log in on the <Text style={[
styles.boldText,
!isMdbListEnabled && styles.disabledBoldText
]}>MDBList website</Text>.
</View>
<View style={styles.stepRow}>
<View style={[styles.stepNumber, { backgroundColor: currentTheme.colors.primary }]}>
<Text style={styles.stepNumberText}>2</Text>
</View>
<Text style={[styles.stepText, { color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }]}>
{t('mdblist.step_2')} <Text style={{ fontWeight: 'bold', color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }}>{t('mdblist.step_2_settings')}</Text> {'>'} <Text style={{ fontWeight: 'bold', color: isDarkMode ? currentTheme.colors.highEmphasis : currentTheme.colors.textDark }}>{t('mdblist.step_2_api')}</Text> {t('mdblist.step_2_end')}
</Text>
</View>
<View style={styles.infoStep}>
<Text style={[
styles.infoStepNumber,
!isMdbListEnabled && styles.disabledText
]}>
2.
</Text>
<Text style={[
styles.infoStepText,
!isMdbListEnabled && styles.disabledText
]}>
Go to <Text style={[
styles.boldText,
!isMdbListEnabled && styles.disabledBoldText
]}>Settings</Text> {'>'} <Text style={[
styles.boldText,
!isMdbListEnabled && styles.disabledBoldText
]}>API</Text> section.
</Text>
</View>
<View style={styles.infoStep}>
<Text style={[
styles.infoStepNumber,
!isMdbListEnabled && styles.disabledText
]}>
3.
</Text>
<Text style={[
styles.infoStepText,
!isMdbListEnabled && styles.disabledText
]}>
Generate a new key and copy it.
</View>
<View style={styles.stepRow}>
<View style={[styles.stepNumber, { backgroundColor: currentTheme.colors.primary }]}>
<Text style={styles.stepNumberText}>3</Text>
</View>
<Text style={[styles.stepText, { color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }]}>
{t('mdblist.step_3')}
</Text>
</View>
</View>
@ -811,29 +871,29 @@ const MDBListSettingsScreen = () => {
onPress={openMDBListWebsite}
disabled={!isMdbListEnabled}
>
<MaterialIcons
name="open-in-new"
size={18}
color={!isMdbListEnabled ? colors.darkGray : colors.primary}
style={styles.buttonIcon}
<MaterialIcons
name="open-in-new"
size={18}
color={!isMdbListEnabled ? currentTheme.colors.mediumEmphasis : currentTheme.colors.primary}
style={styles.buttonIcon}
/>
<Text style={[
styles.websiteButtonText,
!isMdbListEnabled && styles.websiteButtonTextDisabled
]}>
Go to MDBList
{t('mdblist.go_to_website')}
</Text>
</TouchableOpacity>
</View>
</ScrollView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};

View file

@ -17,10 +17,12 @@ import { notificationService, NotificationSettings } from '../services/notificat
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';
import { useNavigation } from '@react-navigation/native';
import { logger } from '../utils/logger';
import { useTranslation } from 'react-i18next';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
const NotificationSettingsScreen = () => {
const { t } = useTranslation();
const navigation = useNavigation();
const { currentTheme } = useTheme();
const [settings, setSettings] = useState<NotificationSettings>({
@ -47,7 +49,7 @@ const NotificationSettingsScreen = () => {
try {
const savedSettings = await notificationService.getSettings();
setSettings(savedSettings);
// Load notification stats
const stats = notificationService.getNotificationStats();
setNotificationStats(stats);
@ -72,7 +74,7 @@ const NotificationSettingsScreen = () => {
// Add countdown effect
useEffect(() => {
let intervalId: NodeJS.Timeout;
if (countdown !== null && countdown > 0) {
intervalId = setInterval(() => {
setCountdown(prev => prev !== null ? prev - 1 : null);
@ -96,23 +98,23 @@ const NotificationSettingsScreen = () => {
...settings,
[key]: value,
};
// Special case: if enabling notifications, make sure permissions are granted
if (key === 'enabled' && value === true) {
// Permissions are handled in the service
}
// Update settings in the service
await notificationService.updateSettings({ [key]: value });
// Update local state
setSettings(updatedSettings);
} catch (error) {
logger.error('Error updating notification settings:', error);
setAlertTitle('Error');
setAlertMessage('Failed to update notification settings');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
setAlertTitle('Error');
setAlertMessage('Failed to update notification settings');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
};
@ -122,20 +124,20 @@ const NotificationSettingsScreen = () => {
};
const resetAllNotifications = async () => {
setAlertTitle('Reset Notifications');
setAlertMessage('This will cancel all scheduled notifications, but will not remove anything from your saved library. Are you sure?');
setAlertTitle(t('notification.alert_reset_title'));
setAlertMessage(t('notification.alert_reset_msg'));
setAlertActions([
{ label: 'Cancel', onPress: () => setAlertVisible(false), style: { color: currentTheme.colors.mediumGray } },
{
label: 'Reset',
label: t('mdblist.reset_confirm') || 'Reset', // Using mdblist or common if available, fallback for safely
onPress: async () => {
try {
const scheduledNotifications = notificationService.getScheduledNotifications?.() || [];
for (const notification of scheduledNotifications) {
await notificationService.cancelNotification(notification.id);
}
setAlertTitle('Success');
setAlertMessage('All notifications have been reset');
setAlertTitle(t('common.success') || 'Success');
setAlertMessage(t('notification.alert_reset_success'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
@ -154,25 +156,25 @@ const NotificationSettingsScreen = () => {
const handleSyncNotifications = async () => {
if (isSyncing) return;
setIsSyncing(true);
try {
await notificationService.syncAllNotifications();
// Refresh stats after sync
const stats = notificationService.getNotificationStats();
setNotificationStats(stats);
setAlertTitle('Sync Complete');
setAlertMessage(`Successfully synced notifications for your library and Trakt items.\n\nScheduled: ${stats.upcoming} upcoming episodes\nThis week: ${stats.thisWeek} episodes`);
setAlertTitle(t('notification.alert_sync_complete'));
setAlertMessage(t('notification.alert_sync_msg', { upcoming: stats.upcoming, thisWeek: stats.thisWeek }));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} catch (error) {
logger.error('Error syncing notifications:', error);
setAlertTitle('Error');
setAlertMessage('Failed to sync notifications. Please try again.');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
setAlertTitle('Error');
setAlertMessage('Failed to sync notifications. Please try again.');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} finally {
setIsSyncing(false);
}
@ -224,22 +226,22 @@ const NotificationSettingsScreen = () => {
if (notificationId) {
setTestNotificationId(notificationId);
setCountdown(0); // No countdown for instant notification
setAlertTitle('Success');
setAlertMessage('Test notification scheduled to fire instantly');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
setAlertTitle(t('common.success') || 'Success');
setAlertMessage(t('notification.alert_test_scheduled'));
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
} else {
setAlertTitle('Error');
setAlertMessage('Failed to schedule test notification. Make sure notifications are enabled.');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
setAlertTitle('Error');
setAlertMessage('Failed to schedule test notification. Make sure notifications are enabled.');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
} catch (error) {
logger.error('Error scheduling test notification:', error);
setAlertTitle('Error');
setAlertMessage('Failed to schedule test notification');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
setAlertTitle('Error');
setAlertMessage('Failed to schedule test notification');
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
setAlertVisible(true);
}
};
@ -247,13 +249,13 @@ const NotificationSettingsScreen = () => {
return (
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<View style={[styles.header, { borderBottomColor: currentTheme.colors.border }]}>
<TouchableOpacity
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
</TouchableOpacity>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>Notification Settings</Text>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>{t('notification.title')}</Text>
<View style={{ width: 40 }} />
</View>
<View style={styles.loadingContainer}>
@ -266,39 +268,39 @@ const NotificationSettingsScreen = () => {
return (
<SafeAreaView style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar barStyle="light-content" />
<View style={styles.header}>
<TouchableOpacity
<TouchableOpacity
style={styles.backButton}
onPress={() => navigation.goBack()}
>
<MaterialIcons name="arrow-back" size={24} color={currentTheme.colors.text} />
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
Settings
{t('common.settings') || 'Settings'}
</Text>
</TouchableOpacity>
<View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */}
</View>
</View>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
Notification Settings
{t('notification.title')}
</Text>
<ScrollView style={styles.content}>
<Animated.View
<Animated.View
entering={FadeIn.duration(300)}
exiting={FadeOut.duration(200)}
>
<View style={[styles.section, { borderBottomColor: currentTheme.colors.border }]}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.text }]}>General</Text>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.text }]}>{t('notification.section_general')}</Text>
<View style={[styles.settingItem, { borderBottomColor: currentTheme.colors.border + '50' }]}>
<View style={styles.settingInfo}>
<MaterialIcons name="notifications" size={24} color={currentTheme.colors.text} />
<Text style={[styles.settingText, { color: currentTheme.colors.text }]}>Enable Notifications</Text>
<Text style={[styles.settingText, { color: currentTheme.colors.text }]}>{t('notification.enable_notifications')}</Text>
</View>
<Switch
value={settings.enabled}
@ -308,16 +310,16 @@ const NotificationSettingsScreen = () => {
/>
</View>
</View>
{settings.enabled && (
<>
<View style={[styles.section, { borderBottomColor: currentTheme.colors.border }]}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.text }]}>Notification Types</Text>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.text }]}>{t('notification.section_types')}</Text>
<View style={[styles.settingItem, { borderBottomColor: currentTheme.colors.border + '50' }]}>
<View style={styles.settingInfo}>
<MaterialIcons name="new-releases" size={24} color={currentTheme.colors.text} />
<Text style={[styles.settingText, { color: currentTheme.colors.text }]}>New Episodes</Text>
<Text style={[styles.settingText, { color: currentTheme.colors.text }]}>{t('notification.new_episodes')}</Text>
</View>
<Switch
value={settings.newEpisodeNotifications}
@ -326,11 +328,11 @@ const NotificationSettingsScreen = () => {
thumbColor={settings.newEpisodeNotifications ? currentTheme.colors.primary : currentTheme.colors.lightGray}
/>
</View>
<View style={[styles.settingItem, { borderBottomColor: currentTheme.colors.border + '50' }]}>
<View style={styles.settingInfo}>
<MaterialIcons name="event" size={24} color={currentTheme.colors.text} />
<Text style={[styles.settingText, { color: currentTheme.colors.text }]}>Upcoming Shows</Text>
<Text style={[styles.settingText, { color: currentTheme.colors.text }]}>{t('notification.upcoming_shows')}</Text>
</View>
<Switch
value={settings.upcomingShowsNotifications}
@ -339,11 +341,11 @@ const NotificationSettingsScreen = () => {
thumbColor={settings.upcomingShowsNotifications ? currentTheme.colors.primary : currentTheme.colors.lightGray}
/>
</View>
<View style={[styles.settingItem, { borderBottomColor: currentTheme.colors.border + '50' }]}>
<View style={styles.settingInfo}>
<MaterialIcons name="alarm" size={24} color={currentTheme.colors.text} />
<Text style={[styles.settingText, { color: currentTheme.colors.text }]}>Reminders</Text>
<Text style={[styles.settingText, { color: currentTheme.colors.text }]}>{t('notification.reminders')}</Text>
</View>
<Switch
value={settings.reminderNotifications}
@ -353,23 +355,23 @@ const NotificationSettingsScreen = () => {
/>
</View>
</View>
<View style={[styles.section, { borderBottomColor: currentTheme.colors.border }]}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.text }]}>Notification Timing</Text>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.text }]}>{t('notification.section_timing')}</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.lightGray }]}>
When should you be notified before an episode airs?
{t('notification.timing_desc')}
</Text>
<View style={styles.timingOptions}>
{[1, 6, 12, 24].map((hours) => (
<TouchableOpacity
key={hours}
style={[
styles.timingOption,
{
{
backgroundColor: currentTheme.colors.elevation1,
borderColor: currentTheme.colors.border
borderColor: currentTheme.colors.border
},
settings.timeBeforeAiring === hours && {
backgroundColor: currentTheme.colors.primary + '30',
@ -386,38 +388,38 @@ const NotificationSettingsScreen = () => {
fontWeight: 'bold',
}
]}>
{hours === 1 ? '1 hour' : `${hours} hours`}
{hours === 1 ? t('notification.hours_1') : `${hours} ${t('notification.hours_suffix')}`}
</Text>
</TouchableOpacity>
))}
</View>
</View>
<View style={[styles.section, { borderBottomColor: currentTheme.colors.border }]}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.text }]}>Notification Status</Text>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.text }]}>{t('notification.section_status')}</Text>
<View style={[styles.statsContainer, { backgroundColor: currentTheme.colors.elevation1 }]}>
<View style={styles.statItem}>
<MaterialIcons name="schedule" size={20} color={currentTheme.colors.primary} />
<Text style={[styles.statLabel, { color: currentTheme.colors.textMuted }]}>Upcoming</Text>
<Text style={[styles.statLabel, { color: currentTheme.colors.textMuted }]}>{t('notification.stats_upcoming')}</Text>
<Text style={[styles.statValue, { color: currentTheme.colors.text }]}>{notificationStats.upcoming}</Text>
</View>
<View style={styles.statItem}>
<MaterialIcons name="today" size={20} color={currentTheme.colors.primary} />
<Text style={[styles.statLabel, { color: currentTheme.colors.textMuted }]}>This Week</Text>
<Text style={[styles.statLabel, { color: currentTheme.colors.textMuted }]}>{t('notification.stats_this_week')}</Text>
<Text style={[styles.statValue, { color: currentTheme.colors.text }]}>{notificationStats.thisWeek}</Text>
</View>
<View style={styles.statItem}>
<MaterialIcons name="notifications-active" size={20} color={currentTheme.colors.primary} />
<Text style={[styles.statLabel, { color: currentTheme.colors.textMuted }]}>Total</Text>
<Text style={[styles.statLabel, { color: currentTheme.colors.textMuted }]}>{t('notification.stats_total')}</Text>
<Text style={[styles.statValue, { color: currentTheme.colors.text }]}>{notificationStats.total}</Text>
</View>
</View>
<TouchableOpacity
<TouchableOpacity
style={[
styles.resetButton,
{
{
backgroundColor: currentTheme.colors.primary + '20',
borderColor: currentTheme.colors.primary + '50'
}
@ -425,29 +427,29 @@ const NotificationSettingsScreen = () => {
onPress={handleSyncNotifications}
disabled={isSyncing}
>
<MaterialIcons
name={isSyncing ? "sync" : "sync"}
size={24}
<MaterialIcons
name={isSyncing ? "sync" : "sync"}
size={24}
color={currentTheme.colors.primary}
style={isSyncing ? { transform: [{ rotate: '360deg' }] } : {}}
/>
<Text style={[styles.resetButtonText, { color: currentTheme.colors.primary }]}>
{isSyncing ? 'Syncing...' : 'Sync Library & Trakt'}
{isSyncing ? t('notification.syncing') : t('notification.sync_button')}
</Text>
</TouchableOpacity>
<Text style={[styles.resetDescription, { color: currentTheme.colors.lightGray }]}>
Automatically syncs notifications for all shows in your library and Trakt watchlist/collection.
{t('notification.sync_desc')}
</Text>
</View>
<View style={[styles.section, { borderBottomColor: currentTheme.colors.border }]}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.text }]}>Advanced</Text>
<TouchableOpacity
<Text style={[styles.sectionTitle, { color: currentTheme.colors.text }]}>{t('notification.section_advanced')}</Text>
<TouchableOpacity
style={[
styles.resetButton,
{
{
backgroundColor: currentTheme.colors.error + '20',
borderColor: currentTheme.colors.error + '50'
}
@ -455,13 +457,13 @@ const NotificationSettingsScreen = () => {
onPress={resetAllNotifications}
>
<MaterialIcons name="refresh" size={24} color={currentTheme.colors.error} />
<Text style={[styles.resetButtonText, { color: currentTheme.colors.error }]}>Reset All Notifications</Text>
<Text style={[styles.resetButtonText, { color: currentTheme.colors.error }]}>{t('notification.reset_button')}</Text>
</TouchableOpacity>
<TouchableOpacity
<TouchableOpacity
style={[
styles.resetButton,
{
styles.resetButton,
{
marginTop: 12,
backgroundColor: currentTheme.colors.primary + '20',
borderColor: currentTheme.colors.primary + '50'
@ -472,22 +474,22 @@ const NotificationSettingsScreen = () => {
>
<MaterialIcons name="bug-report" size={24} color={currentTheme.colors.primary} />
<Text style={[styles.resetButtonText, { color: currentTheme.colors.primary }]}>
{countdown !== null
? `Notification in ${countdown}s...`
: 'Test Notification (5 sec)'}
{countdown !== null
? t('notification.test_notification_in', { seconds: countdown })
: t('notification.test_button')}
</Text>
</TouchableOpacity>
{countdown !== null && (
<View style={styles.countdownContainer}>
<MaterialIcons
name="timer"
size={16}
color={currentTheme.colors.primary}
style={styles.countdownIcon}
<MaterialIcons
name="timer"
size={16}
color={currentTheme.colors.primary}
style={styles.countdownIcon}
/>
<Text style={[styles.countdownText, { color: currentTheme.colors.primary }]}>
Notification will appear in {countdown} seconds
{t('notification.test_notification_text', { seconds: countdown })}
</Text>
</View>
)}
@ -496,14 +498,14 @@ const NotificationSettingsScreen = () => {
)}
</Animated.View>
</ScrollView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};

View file

@ -15,6 +15,7 @@ import { useSettings, AppSettings } from '../hooks/useSettings';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
import { useTheme } from '../contexts/ThemeContext';
import CustomAlert from '../components/CustomAlert';
import { useTranslation } from 'react-i18next';
const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0;
@ -95,6 +96,7 @@ const PlayerSettingsScreen: React.FC = () => {
const { settings, updateSetting } = useSettings();
const { currentTheme } = useTheme();
const navigation = useNavigation();
const { t } = useTranslation();
// CustomAlert state
const [alertVisible, setAlertVisible] = useState(false);
@ -110,46 +112,46 @@ const PlayerSettingsScreen: React.FC = () => {
const playerOptions = [
{
id: 'internal',
title: 'Built-in Player',
description: 'Use the app\'s default video player',
title: t('player.internal_title'),
description: t('player.internal_desc'),
icon: 'play-circle-outline',
},
...(Platform.OS === 'ios' ? [
{
id: 'vlc',
title: 'VLC',
description: 'Open streams in VLC media player',
title: t('player.vlc_title'),
description: t('player.vlc_desc'),
icon: 'video-library',
},
{
id: 'infuse',
title: 'Infuse',
description: 'Open streams in Infuse player',
title: t('player.infuse_title'),
description: t('player.infuse_desc'),
icon: 'smart-display',
},
{
id: 'outplayer',
title: 'OutPlayer',
description: 'Open streams in OutPlayer',
title: t('player.outplayer_title'),
description: t('player.outplayer_desc'),
icon: 'slideshow',
},
{
id: 'vidhub',
title: 'VidHub',
description: 'Open streams in VidHub player',
title: t('player.vidhub_title'),
description: t('player.vidhub_desc'),
icon: 'ondemand-video',
},
{
id: 'infuse_livecontainer',
title: 'Infuse Livecontainer',
description: 'Open streams in Infuse player LiveContainer',
title: t('player.infuse_live_title'),
description: t('player.infuse_live_desc'),
icon: 'smart-display',
},
] : [
{
id: 'external',
title: 'External Player',
description: 'Open streams in your preferred video player',
title: t('player.external_title'),
description: t('player.external_desc'),
icon: 'open-in-new',
},
]),
@ -184,7 +186,7 @@ const PlayerSettingsScreen: React.FC = () => {
color={currentTheme.colors.text}
/>
<Text style={[styles.backText, { color: currentTheme.colors.text }]}>
Settings
{t('common.settings') || 'Settings'}
</Text>
</TouchableOpacity>
@ -194,7 +196,7 @@ const PlayerSettingsScreen: React.FC = () => {
</View>
<Text style={[styles.headerTitle, { color: currentTheme.colors.text }]}>
Video Player
{t('player.title')}
</Text>
<ScrollView
@ -208,7 +210,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.textMuted },
]}
>
PLAYER SELECTION
{t('player.section_selection')}
</Text>
<View
style={[
@ -249,7 +251,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.textMuted },
]}
>
PLAYBACK OPTIONS
{t('player.section_playback')}
</Text>
<View
style={[
@ -278,7 +280,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.text },
]}
>
Auto-play Best Stream
{t('player.autoplay_title')}
</Text>
<Text
style={[
@ -286,7 +288,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.textMuted },
]}
>
Automatically start the highest quality stream available.
{t('player.autoplay_desc')}
</Text>
</View>
<Switch
@ -316,7 +318,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.text },
]}
>
Always Resume
{t('player.resume_title')}
</Text>
<Text
style={[
@ -324,7 +326,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.textMuted },
]}
>
Skip the resume prompt and automatically continue where you left off (if less than 85% watched).
{t('player.resume_desc')}
</Text>
</View>
<Switch
@ -357,7 +359,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.text },
]}
>
Video Player Engine
{t('player.engine_title')}
</Text>
<Text
style={[
@ -365,7 +367,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.textMuted },
]}
>
Auto uses ExoPlayer with MPV fallback. Some formats like Dolby Vision and HDR may not be supported by MPV, so Auto is recommended for best compatibility.
{t('player.engine_desc')}
</Text>
</View>
</View>
@ -416,7 +418,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.text },
]}
>
Decoder Mode
{t('player.decoder_title')}
</Text>
<Text
style={[
@ -424,7 +426,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.textMuted },
]}
>
How video is decoded. Auto is recommended for best balance.
{t('player.decoder_desc')}
</Text>
</View>
</View>
@ -440,8 +442,8 @@ const PlayerSettingsScreen: React.FC = () => {
onPress={() => {
updateSetting('decoderMode', option.id);
openAlert(
'Restart Required',
'Please restart the app for the decoder change to take effect.'
t('player.restart_required'),
t('player.restart_msg_decoder')
);
}}
style={[
@ -482,7 +484,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.text },
]}
>
GPU Rendering
{t('player.gpu_title')}
</Text>
<Text
style={[
@ -490,7 +492,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.textMuted },
]}
>
GPU-Next offers better HDR and color management.
{t('player.gpu_desc')}
</Text>
</View>
</View>
@ -504,8 +506,8 @@ const PlayerSettingsScreen: React.FC = () => {
onPress={() => {
updateSetting('gpuMode', option.id);
openAlert(
'Restart Required',
'Please restart the app for the GPU mode change to take effect.'
t('player.restart_required'),
t('player.restart_msg_gpu')
);
}}
style={[
@ -551,7 +553,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.text },
]}
>
External Player for Downloads
{t('player.external_downloads_title')}
</Text>
<Text
style={[
@ -559,7 +561,7 @@ const PlayerSettingsScreen: React.FC = () => {
{ color: currentTheme.colors.textMuted },
]}
>
Play downloaded content in your preferred external player.
{t('player.external_downloads_desc')}
</Text>
</View>
<Switch

View file

@ -25,6 +25,7 @@ import { useSettings } from '../hooks/useSettings';
import { localScraperService, pluginService, ScraperInfo, RepositoryInfo } from '../services/pluginService';
import { logger } from '../utils/logger';
import { useTheme } from '../contexts/ThemeContext';
import { useTranslation } from 'react-i18next';
const { width: screenWidth } = Dimensions.get('window');
@ -902,6 +903,7 @@ const PluginsScreen: React.FC = () => {
const navigation = useNavigation();
const { settings, updateSetting } = useSettings();
const { currentTheme } = useTheme();
const { t } = useTranslation();
const colors = currentTheme.colors;
const styles = createStyles(colors);
@ -1025,10 +1027,10 @@ const PluginsScreen: React.FC = () => {
);
await Promise.all(promises);
await loadPlugins();
openAlert('Success', `${enabled ? 'Enabled' : 'Disabled'} ${filteredPlugins.length} plugins`);
openAlert(t('plugins.success'), `${enabled ? t('plugins.enabled') : t('plugins.disabled')} ${filteredPlugins.length} plugins`);
} catch (error) {
logger.error('[PluginSettings] Failed to bulk toggle:', error);
openAlert('Error', 'Failed to update plugins');
openAlert(t('plugins.error'), 'Failed to update plugins');
} finally {
setIsRefreshing(false);
}
@ -1048,7 +1050,7 @@ const PluginsScreen: React.FC = () => {
const url = newRepositoryUrl.trim();
if (!url.startsWith('https://raw.githubusercontent.com/') && !url.startsWith('http://')) {
openAlert(
'Invalid URL Format',
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/tapframe/nuvio-providers/refs/heads/master'
);
return;
@ -1089,10 +1091,10 @@ const PluginsScreen: React.FC = () => {
setNewRepositoryUrl('');
setShowAddRepositoryModal(false);
openAlert('Success', 'Repository added and plugins loaded successfully');
openAlert(t('plugins.success'), t('plugins.alert_repo_added'));
} catch (error) {
logger.error('[PluginsScreen] Failed to add repository:', error);
openAlert('Error', 'Failed to add repository');
openAlert(t('plugins.error'), 'Failed to add repository');
} finally {
setIsLoading(false);
}
@ -1113,10 +1115,10 @@ const PluginsScreen: React.FC = () => {
await loadPlugins();
const repo = repositories.find(r => r.id === repoId);
openAlert('Success', `Repository "${repo?.name || 'Unknown'}" ${enabled ? 'enabled' : 'disabled'} successfully`);
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('Error', 'Failed to update repository');
openAlert(t('plugins.error'), 'Failed to update repository');
} finally {
setSwitchingRepository(null);
}
@ -1249,10 +1251,10 @@ const PluginsScreen: React.FC = () => {
await pluginService.setRepositoryUrl(url);
await updateSetting('scraperRepositoryUrl', url);
setHasRepository(true);
openAlert('Success', 'Repository URL saved successfully');
openAlert(t('plugins.success'), t('plugins.alert_repo_saved'));
} catch (error) {
logger.error('[PluginSettings] Failed to save repository:', error);
openAlert('Error', 'Failed to save repository URL');
openAlert(t('plugins.error'), 'Failed to save repository URL');
} finally {
setIsLoading(false);
}
@ -1274,7 +1276,7 @@ const PluginsScreen: React.FC = () => {
// Load fresh plugins from the updated repository
await loadPlugins();
openAlert('Success', 'Repository refreshed successfully with latest files');
openAlert(t('plugins.success'), t('plugins.alert_repo_refreshed'));
} catch (error) {
logger.error('[PluginsScreen] Failed to refresh repository:', error);
const errorMessage = error instanceof Error ? error.message : String(error);
@ -1306,15 +1308,15 @@ const PluginsScreen: React.FC = () => {
await loadPlugins();
} catch (error) {
logger.error('[PluginSettings] Failed to toggle plugin:', error);
openAlert('Error', 'Failed to update plugin status');
openAlert(t('plugins.error'), 'Failed to update plugin status');
setIsRefreshing(false);
}
};
const handleClearPlugins = () => {
openAlert(
'Clear All Plugins',
'Are you sure you want to remove all installed plugins? This action cannot be undone.',
t('plugins.clear_all'),
t('plugins.clear_all_desc'),
[
{ label: 'Cancel', onPress: () => { } },
{
@ -1323,10 +1325,10 @@ const PluginsScreen: React.FC = () => {
try {
await pluginService.clearScrapers();
await loadPlugins();
openAlert('Success', 'All plugins have been removed');
openAlert(t('plugins.success'), t('plugins.alert_plugins_cleared'));
} catch (error) {
logger.error('[PluginSettings] Failed to clear plugins:', error);
openAlert('Error', 'Failed to clear plugins');
openAlert(t('plugins.error'), 'Failed to clear plugins');
}
},
},
@ -1336,8 +1338,8 @@ const PluginsScreen: React.FC = () => {
const handleClearPluginCache = () => {
openAlert(
'Clear Repository Cache',
'This will remove the saved repository URL and clear all cached plugin data. You will need to re-enter your repository URL.',
t('plugins.clear_cache'),
t('plugins.clear_cache_desc'),
[
{ label: 'Cancel', onPress: () => { } },
{
@ -1350,10 +1352,10 @@ const PluginsScreen: React.FC = () => {
setRepositoryUrl('');
setHasRepository(false);
await loadPlugins();
openAlert('Success', 'Repository cache cleared successfully');
openAlert(t('plugins.success'), t('plugins.alert_cache_cleared'));
} catch (error) {
logger.error('[PluginSettings] Failed to clear cache:', error);
openAlert('Error', 'Failed to clear repository cache');
openAlert(t('plugins.error'), 'Failed to clear repository cache');
}
},
},

View file

@ -338,7 +338,7 @@ const SettingsScreen: React.FC = () => {
<SettingsCard title={t('settings.sections.account')} isTablet={isTablet}>
{isItemVisible('trakt') && (
<SettingItem
title="Trakt"
title={t('settings.trakt')}
description={isAuthenticated ? `@${userProfile?.username || 'User'}` : t('settings.sign_in_sync')}
customIcon={<TraktIcon size={isTablet ? 24 : 20} color={currentTheme.colors.primary} />}
renderControl={() => <ChevronRight />}
@ -420,8 +420,8 @@ const SettingsScreen: React.FC = () => {
icon="trash-2"
onPress={() => {
openAlert(
'Clear All Data',
'This will reset all settings and clear all cached data. Are you sure?',
t('settings.clear_data'),
t('settings.clear_data_desc'),
[
{ label: 'Cancel', onPress: () => { } },
{
@ -476,7 +476,7 @@ const SettingsScreen: React.FC = () => {
return (
<SettingsCard title={t('settings.updates').toUpperCase()} isTablet={isTablet}>
<SettingItem
title="App Updates"
title={t('settings.app_updates')}
description={t('settings.check_updates')}
icon="refresh-ccw"
renderControl={() => <ChevronRight />}
@ -565,7 +565,7 @@ const SettingsScreen: React.FC = () => {
<SettingsCard title={t('settings.account').toUpperCase()}>
{isItemVisible('trakt') && (
<SettingItem
title="Trakt"
title={t('settings.trakt')}
description={isAuthenticated ? `@${userProfile?.username || 'User'}` : t('settings.sign_in_sync')}
customIcon={<TraktIcon size={20} color={currentTheme.colors.primary} />}
renderControl={() => <ChevronRight />}
@ -648,7 +648,7 @@ const SettingsScreen: React.FC = () => {
)}
{(settingsConfig?.categories?.['updates']?.visible !== false) && (
<SettingItem
title="App Updates"
title={t('settings.app_updates')}
description={t('settings.check_updates')}
icon="refresh-ccw"
badge={Platform.OS === 'android' && hasUpdateBadge ? 1 : undefined}
@ -681,7 +681,7 @@ const SettingsScreen: React.FC = () => {
{/* About */}
<SettingsCard title={t('settings.about').toUpperCase()}>
<SettingItem
title="About Nuvio"
title={t('settings.about_nuvio')}
description={getDisplayedAppVersion()}
icon="info"
renderControl={() => <ChevronRight />}