import React, { useState, useEffect } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Switch, SafeAreaView, Image, Alert, StatusBar, Platform, ActivityIndicator, } from 'react-native'; import { NavigationProp, useNavigation } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useSettings, DEFAULT_SETTINGS } from '../hooks/useSettings'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { TMDBService } from '../services/tmdbService'; import { logger } from '../utils/logger'; import { useTheme } from '../contexts/ThemeContext'; // TMDB API key - since the default key might be private in the service, we'll use our own const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c'; // Define example shows with their IMDB IDs and TMDB IDs const EXAMPLE_SHOWS = [ { name: 'Breaking Bad', imdbId: 'tt0903747', tmdbId: '1396', type: 'tv' as const }, { name: 'Friends', imdbId: 'tt0108778', tmdbId: '1668', type: 'tv' as const }, { name: 'Game of Thrones', imdbId: 'tt0944947', tmdbId: '1399', type: 'tv' as const }, { name: 'Stranger Things', imdbId: 'tt4574334', tmdbId: '66732', type: 'tv' as const }, { name: 'Squid Game', imdbId: 'tt10919420', tmdbId: '93405', type: 'tv' as const }, { name: 'Avatar', imdbId: 'tt0499549', tmdbId: '19995', type: 'movie' as const }, { name: 'The Witcher', imdbId: 'tt5180504', tmdbId: '71912', type: 'tv' as const } ]; // Create a styles creator function that accepts the theme colors const createStyles = (colors: any) => StyleSheet.create({ container: { flex: 1, backgroundColor: colors.darkBackground, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingTop: Platform.OS === 'android' ? (StatusBar.currentHeight || 0) + 8 : 8, backgroundColor: colors.darkBackground, }, backButton: { flexDirection: 'row', alignItems: 'center', padding: 8, }, backText: { fontSize: 17, marginLeft: 8, color: colors.white, }, headerActions: { flexDirection: 'row', alignItems: 'center', }, headerButton: { padding: 8, marginLeft: 8, }, headerTitle: { fontSize: 34, fontWeight: 'bold', paddingHorizontal: 16, marginBottom: 24, color: colors.white, }, headerRight: { width: 24, }, scrollView: { flex: 1, }, scrollContent: { paddingHorizontal: 16, paddingBottom: 24, }, descriptionContainer: { marginBottom: 16, }, description: { color: colors.mediumEmphasis, fontSize: 15, lineHeight: 22, }, showSelectorContainer: { marginBottom: 16, }, selectorLabel: { color: colors.highEmphasis, fontSize: 16, fontWeight: '500', marginBottom: 12, }, showsScrollContent: { paddingRight: 16, }, showItem: { paddingHorizontal: 12, paddingVertical: 6, backgroundColor: colors.elevation2, borderRadius: 16, marginRight: 6, borderWidth: 1, borderColor: 'transparent', shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 1, elevation: 1, }, selectedShowItem: { borderColor: colors.primary, backgroundColor: colors.elevation3, shadowColor: colors.primary, shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.2, shadowRadius: 2, elevation: 2, }, showItemText: { color: colors.mediumEmphasis, fontSize: 14, }, selectedShowItemText: { color: colors.white, fontWeight: '600', }, optionsContainer: { marginBottom: 16, gap: 12, }, optionCard: { backgroundColor: colors.elevation2, borderRadius: 8, padding: 12, borderWidth: 2, borderColor: 'transparent', marginBottom: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.2, shadowRadius: 3, elevation: 2, }, selectedCard: { borderColor: colors.primary, shadowColor: colors.primary, shadowOpacity: 0.3, elevation: 3, }, optionHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6, }, optionTitle: { color: colors.white, fontSize: 16, fontWeight: '600', }, optionDescription: { color: colors.mediumEmphasis, fontSize: 13, lineHeight: 18, marginBottom: 10, }, exampleContainer: { marginTop: 4, }, exampleLabel: { color: colors.mediumEmphasis, fontSize: 13, marginBottom: 4, }, exampleImage: { height: 60, width: '100%', backgroundColor: 'rgba(0,0,0,0.5)', borderRadius: 8, }, loadingContainer: { justifyContent: 'center', alignItems: 'center', }, infoBox: { marginBottom: 16, padding: 12, backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 8, borderLeftWidth: 3, borderLeftColor: colors.primary, }, infoText: { color: colors.mediumEmphasis, fontSize: 12, lineHeight: 18, }, logoSourceLabel: { color: colors.mediumEmphasis, fontSize: 11, marginTop: 2, }, languageSelectorContainer: { marginTop: 10, padding: 10, backgroundColor: 'rgba(255,255,255,0.05)', borderRadius: 6, }, languageSelectorTitle: { color: colors.white, fontSize: 14, fontWeight: '600', marginBottom: 4, }, languageSelectorDescription: { color: colors.mediumEmphasis, fontSize: 12, lineHeight: 18, marginBottom: 8, }, languageSelectorLabel: { color: colors.mediumEmphasis, fontSize: 12, marginBottom: 6, }, languageScrollContent: { paddingVertical: 2, }, languageItem: { paddingHorizontal: 10, paddingVertical: 6, backgroundColor: colors.elevation1, borderRadius: 12, marginRight: 6, borderWidth: 1, borderColor: colors.elevation3, marginVertical: 1, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 1, elevation: 1, }, selectedLanguageItem: { backgroundColor: colors.primary, borderColor: colors.primary, shadowColor: colors.primary, shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.2, shadowRadius: 1, elevation: 2, }, languageItemText: { color: colors.mediumEmphasis, fontSize: 12, fontWeight: '600', }, selectedLanguageItemText: { color: colors.white, }, noteText: { color: colors.mediumEmphasis, fontSize: 11, marginTop: 8, fontStyle: 'italic', }, bannerContainer: { height: 90, width: '100%', borderRadius: 6, overflow: 'hidden', position: 'relative', }, bannerImage: { ...StyleSheet.absoluteFillObject, }, bannerOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)', }, logoOverBanner: { position: 'absolute', width: '80%', height: '75%', alignSelf: 'center', top: '12.5%', }, noLogoContainer: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, justifyContent: 'center', alignItems: 'center', }, noLogoText: { color: colors.white, fontSize: 14, backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 4, }, }); const LogoSourceSettings = () => { const { settings, updateSetting } = useSettings(); const navigation = useNavigation>(); const insets = useSafeAreaInsets(); const { currentTheme } = useTheme(); const colors = currentTheme.colors; const styles = createStyles(colors); // Get current preference const [logoSource, setLogoSource] = useState<'metahub' | 'tmdb'>( settings.logoSourcePreference || 'metahub' ); // Make sure logoSource stays in sync with settings useEffect(() => { setLogoSource(settings.logoSourcePreference || 'metahub'); }, [settings.logoSourcePreference]); // Selected example show const [selectedShow, setSelectedShow] = useState(EXAMPLE_SHOWS[0]); // Add state for example logos and banners const [tmdbLogo, setTmdbLogo] = useState(null); const [metahubLogo, setMetahubLogo] = useState(null); const [tmdbBanner, setTmdbBanner] = useState(null); const [metahubBanner, setMetahubBanner] = useState(null); const [loadingLogos, setLoadingLogos] = useState(true); // State for TMDB language selection // Store unique language codes as strings const [uniqueTmdbLanguages, setUniqueTmdbLanguages] = useState([]); const [tmdbLogosData, setTmdbLogosData] = useState | null>(null); // Load example logos for selected show useEffect(() => { fetchExampleLogos(selectedShow); }, [selectedShow]); // Function to fetch logos and banners for a specific show const fetchExampleLogos = async (show: typeof EXAMPLE_SHOWS[0]) => { setLoadingLogos(true); setTmdbLogo(null); setMetahubLogo(null); setTmdbBanner(null); setMetahubBanner(null); // Reset unique languages and logos data setUniqueTmdbLanguages([]); setTmdbLogosData(null); try { const tmdbService = TMDBService.getInstance(); const imdbId = show.imdbId; const tmdbId = show.tmdbId; const contentType = show.type; logger.log(`[LogoSourceSettings] Fetching ${show.name} with TMDB ID: ${tmdbId}, IMDB ID: ${imdbId}`); // Get preferred language directly from settings const preferredTmdbLanguage = settings.tmdbLanguagePreference || 'en'; // Get TMDB logo and banner try { const apiKey = TMDB_API_KEY; const endpoint = contentType === 'tv' ? 'tv' : 'movie'; const response = await fetch(`https://api.themoviedb.org/3/${endpoint}/${tmdbId}/images?api_key=${apiKey}`); const imagesData = await response.json(); // Store all TMDB logos data and extract unique languages if (imagesData.logos && imagesData.logos.length > 0) { setTmdbLogosData(imagesData.logos); // Filter for logos with valid language codes and get unique codes const validLogoLanguages = imagesData.logos .map((logo: { iso_639_1: string | null }) => logo.iso_639_1) .filter((lang: string | null): lang is string => lang !== null && typeof lang === 'string'); // Explicitly type the Set and resulting array const uniqueCodes: string[] = [...new Set(validLogoLanguages)]; setUniqueTmdbLanguages(uniqueCodes); // Find initial logo (prefer selectedTmdbLanguage, then 'en') let initialLogoPath: string | null = null; let initialLanguage = preferredTmdbLanguage; // First try to find a logo in the user's preferred language const preferredLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === preferredTmdbLanguage); if (preferredLogo) { initialLogoPath = preferredLogo.file_path; initialLanguage = preferredTmdbLanguage; logger.log(`[LogoSourceSettings] Found initial ${preferredTmdbLanguage} TMDB logo for ${show.name}`); } else { // Fallback to English logo const englishLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === 'en'); if (englishLogo) { initialLogoPath = englishLogo.file_path; initialLanguage = 'en'; logger.log(`[LogoSourceSettings] Found initial English TMDB logo for ${show.name}`); } else if (imagesData.logos[0]) { // Fallback to the first available logo initialLogoPath = imagesData.logos[0].file_path; initialLanguage = imagesData.logos[0].iso_639_1; logger.log(`[LogoSourceSettings] No English logo, using first available (${initialLanguage}) TMDB logo for ${show.name}`); } } if (initialLogoPath) { setTmdbLogo(`https://image.tmdb.org/t/p/original${initialLogoPath}`); } else { logger.warn(`[LogoSourceSettings] No valid initial TMDB logo found for ${show.name}`); } } else { logger.warn(`[LogoSourceSettings] No TMDB logos found in response for ${show.name}`); setUniqueTmdbLanguages([]); // Ensure it's empty if no logos } // Get TMDB banner (backdrop) if (imagesData.backdrops && imagesData.backdrops.length > 0) { const backdropPath = imagesData.backdrops[0].file_path; const tmdbBannerUrl = `https://image.tmdb.org/t/p/original${backdropPath}`; setTmdbBanner(tmdbBannerUrl); logger.log(`[LogoSourceSettings] Got ${show.name} TMDB banner: ${tmdbBannerUrl}`); } else { // Try to get backdrop from details const detailsResponse = await fetch(`https://api.themoviedb.org/3/${endpoint}/${tmdbId}?api_key=${apiKey}`); const details = await detailsResponse.json(); if (details.backdrop_path) { const tmdbBannerUrl = `https://image.tmdb.org/t/p/original${details.backdrop_path}`; setTmdbBanner(tmdbBannerUrl); logger.log(`[LogoSourceSettings] Got ${show.name} TMDB banner from details: ${tmdbBannerUrl}`); } } } catch (tmdbError) { logger.error(`[LogoSourceSettings] Error fetching TMDB images:`, tmdbError); } // Get Metahub logo and banner try { // Metahub logo const metahubLogoUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`; const logoResponse = await fetch(metahubLogoUrl, { method: 'HEAD' }); if (logoResponse.ok) { setMetahubLogo(metahubLogoUrl); logger.log(`[LogoSourceSettings] Got ${show.name} Metahub logo: ${metahubLogoUrl}`); } // Metahub banner const metahubBannerUrl = `https://images.metahub.space/background/medium/${imdbId}/img`; const bannerResponse = await fetch(metahubBannerUrl, { method: 'HEAD' }); if (bannerResponse.ok) { setMetahubBanner(metahubBannerUrl); logger.log(`[LogoSourceSettings] Got ${show.name} Metahub banner: ${metahubBannerUrl}`); } else if (tmdbBanner) { // If Metahub banner doesn't exist, use TMDB banner setMetahubBanner(tmdbBanner); } } catch (metahubErr) { logger.error(`[LogoSourceSettings] Error checking Metahub images:`, metahubErr); } } catch (err) { logger.error(`[LogoSourceSettings] Error fetching ${show.name} logos:`, err); } finally { setLoadingLogos(false); } }; // Apply logo source setting and show confirmation const applyLogoSourceSetting = (source: 'metahub' | 'tmdb') => { // Update local state first setLogoSource(source); // Update using the settings hook updateSetting('logoSourcePreference', source); // Also save directly to AsyncStorage for extra assurance try { // Get current settings AsyncStorage.getItem('app_settings').then((settingsJson) => { if (settingsJson) { const currentSettings = JSON.parse(settingsJson); // Update the logo source preference const updatedSettings = { ...currentSettings, logoSourcePreference: source }; // Save back to AsyncStorage AsyncStorage.setItem('app_settings', JSON.stringify(updatedSettings)) .then(() => { logger.log(`[LogoSourceSettings] Successfully saved logo source preference '${source}' to AsyncStorage`); }) .catch((error) => { logger.error(`[LogoSourceSettings] Error saving logo source preference to AsyncStorage:`, error); }); } }).catch((error) => { logger.error(`[LogoSourceSettings] Error getting current settings:`, error); }); // Clear any cached logo data AsyncStorage.removeItem('_last_logos_'); } catch (e) { logger.error(`[LogoSourceSettings] Error in applyLogoSourceSetting:`, e); } // Show confirmation alert Alert.alert( 'Settings Updated', `Logo and background source preference set to ${source === 'metahub' ? 'Metahub' : 'TMDB'}. Changes will apply when you navigate to content.`, [{ text: 'OK' }] ); }; // Handle TMDB language selection const handleTmdbLanguageSelect = (languageCode: string) => { // Update the preview logo if possible if (tmdbLogosData) { const selectedLogoData = tmdbLogosData.find(logo => logo.iso_639_1 === languageCode); if (selectedLogoData) { setTmdbLogo(`https://image.tmdb.org/t/p/original${selectedLogoData.file_path}`); logger.log(`[LogoSourceSettings] Switched TMDB logo preview to language: ${languageCode}`); } else { logger.warn(`[LogoSourceSettings] Could not find logo data for selected language: ${languageCode}`); } } // Then persist the setting globally saveLanguagePreference(languageCode); }; // Get preferred language directly from settings for UI rendering const preferredTmdbLanguage = settings.tmdbLanguagePreference || 'en'; // Save language preference with proper persistence const saveLanguagePreference = async (languageCode: string) => { logger.log(`[LogoSourceSettings] Saving TMDB language preference: ${languageCode}`); try { // First use the settings hook to update the setting - this is crucial updateSetting('tmdbLanguagePreference', languageCode); // Clear any cached logo data await AsyncStorage.removeItem('_last_logos_'); // Show confirmation toast or feedback Alert.alert( 'TMDB Language Updated', `TMDB logo language preference set to ${languageCode.toUpperCase()}. Changes will apply when you navigate to content.`, [{ text: 'OK' }] ); } catch (e) { logger.error(`[LogoSourceSettings] Error in saveLanguagePreference:`, e); // Show error notification Alert.alert( 'Error Saving Preference', 'There was a problem saving your language preference. Please try again.', [{ text: 'OK' }] ); } }; // Save selected show to AsyncStorage to persist across navigation const saveSelectedShow = async (show: typeof EXAMPLE_SHOWS[0]) => { try { await AsyncStorage.setItem('logo_settings_selected_show', show.imdbId); } catch (e) { if (__DEV__) console.error('Error saving selected show:', e); } }; // Load selected show from AsyncStorage on mount useEffect(() => { const loadSelectedShow = async () => { try { const savedShowId = await AsyncStorage.getItem('logo_settings_selected_show'); if (savedShowId) { const foundShow = EXAMPLE_SHOWS.find(show => show.imdbId === savedShowId); if (foundShow) { setSelectedShow(foundShow); } } } catch (e) { if (__DEV__) console.error('Error loading selected show:', e); } }; loadSelectedShow(); }, []); // Update selected show and save to AsyncStorage const handleShowSelect = (show: typeof EXAMPLE_SHOWS[0]) => { setSelectedShow(show); saveSelectedShow(show); }; // Handle back navigation const handleBack = () => { navigation.goBack(); }; // Render logo example with loading state and background const renderLogoExample = (logo: string | null, banner: string | null, isLoading: boolean) => { if (isLoading) { return ( ); } return ( {logo && ( )} {!logo && ( No logo available )} ); }; return ( {/* Header */} Settings {/* Empty for now, but ready for future actions */} Logo Source {/* Description */} Choose the primary source for content logos and backgrounds. The selected source will be used exclusively. {/* Show selector */} Select a show/movie to preview: {EXAMPLE_SHOWS.map((show) => ( handleShowSelect(show)} activeOpacity={0.7} delayPressIn={100} > {show.name} ))} {/* Options */} applyLogoSourceSetting('metahub')} activeOpacity={0.7} delayPressIn={100} > Metahub {logoSource === 'metahub' && ( )} High-quality logos from Metahub. Best for popular titles. Example: {renderLogoExample(metahubLogo, metahubBanner, loadingLogos)} {selectedShow.name} logo from Metahub applyLogoSourceSetting('tmdb')} activeOpacity={0.7} delayPressIn={100} > TMDB {logoSource === 'tmdb' && ( )} Logos from TMDB. Offers localized options and better coverage for recent content. Example: {renderLogoExample(tmdbLogo, tmdbBanner, loadingLogos)} {selectedShow.name} logo from TMDB {/* TMDB Language Selector */} {uniqueTmdbLanguages.length > 1 && ( Logo Language Select your preferred language for TMDB logos. {/* Iterate over unique language codes */} {uniqueTmdbLanguages.map((langCode) => ( handleTmdbLanguageSelect(langCode)} activeOpacity={0.7} delayPressIn={150} > {(langCode || '').toUpperCase() || '??'} ))} If unavailable in preferred language, English will be used as fallback. )} {/* Additional Info */} The app will use only the selected source for logos and backgrounds. If no image is available from your chosen source, a text fallback will be used. ); }; export default LogoSourceSettings;