some tmdb logo fetching logic changes

This commit is contained in:
tapframe 2025-10-08 13:39:49 +05:30
parent 42c236e235
commit 238f08192f
10 changed files with 425 additions and 1016 deletions

View file

@ -253,7 +253,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
setLogoLoadError(false);
}, [featuredContent?.id]);
// Fetch logo based on preference
// Fetch logo when enrichment is enabled; otherwise only use addon logo
useEffect(() => {
if (!featuredContent || logoFetchInProgress.current) return;
@ -267,8 +267,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
const contentData = featuredContent; // Use a clearer variable name
const currentLogo = contentData.logo;
// Get preferences
const logoPreference = settings.logoSourcePreference || 'tmdb';
// Get language preference (only relevant when enrichment is enabled)
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
// If enrichment is disabled, use addon logo and don't fetch from external sources
@ -281,7 +280,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
});
// If we have an addon logo, use it and don't fetch external logos
if (contentData.logo && !isTmdbUrl(contentData.logo)) {
if (contentData.logo) {
logger.info('[FeaturedContent] enrichment disabled, using addon logo', { logo: contentData.logo });
setLogoUrl(contentData.logo);
logoFetchInProgress.current = false;
@ -334,11 +333,11 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
let primaryAttempted = false;
let fallbackAttempted = false;
// --- Logo Fetching Logic ---
logger.debug('[FeaturedContent] fetchLogo:ids', { imdbId, tmdbId, preference: logoPreference, lang: preferredLanguage });
// --- Logo Fetching Logic (TMDB only when enrichment is enabled) ---
logger.debug('[FeaturedContent] fetchLogo:ids', { imdbId, tmdbId, lang: preferredLanguage });
// Only try TMDB if preference is 'tmdb' and we have tmdbId
if (logoPreference === 'tmdb' && tmdbId) {
// Try TMDB if we have a TMDB id
if (tmdbId) {
primaryAttempted = true;
try {
const tmdbService = TMDBService.getInstance();
@ -354,11 +353,11 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
// --- Set Final Logo ---
if (finalLogoUrl) {
setLogoUrl(finalLogoUrl);
logger.info('[FeaturedContent] fetchLogo:done', { id: contentId, result: 'ok', duration: since(t0) });
logger.info('[FeaturedContent] fetchLogo:done', { id: contentId, result: 'tmdb', url: finalLogoUrl, duration: since(t0) });
} else if (currentLogo) {
// Use existing logo only if primary and fallback failed or weren't applicable
setLogoUrl(currentLogo);
logger.info('[FeaturedContent] fetchLogo:done', { id: contentId, result: 'existing', duration: since(t0) });
logger.info('[FeaturedContent] fetchLogo:done', { id: contentId, result: 'addon', url: currentLogo, duration: since(t0) });
} else {
// No logo found from any source
setLogoLoadError(true);
@ -377,7 +376,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
// Trigger fetch when content changes
fetchLogo();
}, [featuredContent, settings.logoSourcePreference, settings.tmdbLanguagePreference, settings.enrichMetadataWithTMDB]);
}, [featuredContent, settings.tmdbLanguagePreference, settings.enrichMetadataWithTMDB]);
// Load poster and logo
useEffect(() => {

View file

@ -50,7 +50,6 @@ export const useCalendarData = (): UseCalendarDataReturn => {
} = useTraktContext();
const fetchCalendarData = useCallback(async (forceRefresh = false) => {
logger.log("[CalendarData] Starting to fetch calendar data");
setLoading(true);
try {
@ -68,14 +67,12 @@ export const useCalendarData = (): UseCalendarDataReturn => {
);
if (cachedData) {
logger.log(`[CalendarData] Using cached data with ${cachedData.length} sections`);
setCalendarData(cachedData);
setLoading(false);
return;
}
}
logger.log("[CalendarData] Fetching fresh data from APIs");
const librarySeries = libraryItems.filter(item => item.type === 'series');
let allSeries: StreamingContent[] = [...librarySeries];

View file

@ -131,9 +131,8 @@ export function useFeaturedContent() {
};
});
// Then fetch logos for each item based on preference
// Then fetch logos for each item (TMDB when enrichment enabled)
const tLogos = Date.now();
const preference = settings.logoSourcePreference || 'tmdb';
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
const fetchLogoForItem = async (item: StreamingContent): Promise<StreamingContent> => {
@ -152,58 +151,21 @@ export function useFeaturedContent() {
return item;
}
if (preference === 'tmdb') {
logger.debug('[useFeaturedContent] logo:try:tmdb', { name: item.name, id: item.id, tmdbId, lang: preferredLanguage });
// Resolve TMDB id if we only have IMDb
if (!tmdbId && imdbId) {
const found = await tmdbService.findTMDBIdByIMDB(imdbId);
tmdbId = found ? String(found) : null;
}
if (!tmdbId) return item;
const logoUrl = tmdbId ? await tmdbService.getContentLogo('movie', tmdbId as string, preferredLanguage) : null;
if (logoUrl) {
logger.debug('[useFeaturedContent] logo:tmdb:ok', { name: item.name, id: item.id, url: logoUrl, lang: preferredLanguage });
return { ...item, logo: logoUrl };
}
// Fallback to Metahub via IMDb ID
if (!imdbId && tmdbId) {
const movieDetails: any = await tmdbService.getMovieDetails(tmdbId);
imdbId = movieDetails?.imdb_id;
}
if (imdbId) {
const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`;
logger.debug('[useFeaturedContent] logo:fallback:metahub', { name: item.name, id: item.id, url: metahubUrl });
return { ...item, logo: metahubUrl };
}
logger.debug('[useFeaturedContent] logo:none', { name: item.name, id: item.id });
return item;
} else {
// preference === 'metahub'
// If have IMDb, use directly
if (!imdbId && tmdbId) {
const movieDetails: any = await tmdbService.getMovieDetails(tmdbId);
imdbId = movieDetails?.imdb_id;
}
if (imdbId) {
const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`;
logger.debug('[useFeaturedContent] logo:metahub:ok', { name: item.name, id: item.id, url: metahubUrl });
return { ...item, logo: metahubUrl };
}
// Fallback to TMDB logo
logger.debug('[useFeaturedContent] logo:metahub:miss → fallback:tmdb', { name: item.name, id: item.id, lang: preferredLanguage });
if (!tmdbId && imdbId) {
const found = await tmdbService.findTMDBIdByIMDB(imdbId);
tmdbId = found ? String(found) : null;
}
if (!tmdbId) return item;
const logoUrl = tmdbId ? await tmdbService.getContentLogo('movie', tmdbId as string, preferredLanguage) : null;
if (logoUrl) {
logger.debug('[useFeaturedContent] logo:tmdb:fallback:ok', { name: item.name, id: item.id, url: logoUrl, lang: preferredLanguage });
return { ...item, logo: logoUrl };
}
logger.debug('[useFeaturedContent] logo:none', { name: item.name, id: item.id });
return item;
// Enrichment path: TMDB only
logger.debug('[useFeaturedContent] logo:try:tmdb', { name: item.name, id: item.id, tmdbId, lang: preferredLanguage });
// Resolve TMDB id if we only have IMDb
if (!tmdbId && imdbId) {
const found = await tmdbService.findTMDBIdByIMDB(imdbId);
tmdbId = found ? String(found) : null;
}
if (!tmdbId) return item;
const logoUrl = tmdbId ? await tmdbService.getContentLogo('movie', tmdbId as string, preferredLanguage) : null;
if (logoUrl) {
logger.debug('[useFeaturedContent] logo:tmdb:ok', { name: item.name, id: item.id, url: logoUrl, lang: preferredLanguage });
return { ...item, logo: logoUrl };
}
logger.debug('[useFeaturedContent] logo:none', { name: item.name, id: item.id });
return item;
} catch (error) {
logger.error('[useFeaturedContent] logo:error', { name: item.name, id: item.id, error: String(error) });
return item;
@ -220,7 +182,17 @@ export function useFeaturedContent() {
logo: item.logo && !isTmdbUrl(item.logo) ? item.logo : undefined
}));
}
logger.info('[useFeaturedContent] logos:resolved', { count: formattedContent.length, duration: `${Date.now() - tLogos}ms`, preference });
logger.info('[useFeaturedContent] logos:resolved', { count: formattedContent.length, duration: `${Date.now() - tLogos}ms` });
try {
const details = formattedContent.slice(0, 20).map((c) => ({
id: c.id,
name: c.name,
hasLogo: Boolean(c.logo),
logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none',
logo: c.logo || undefined,
}));
logger.debug('[useFeaturedContent] logos:details', { items: details });
} catch {}
}
} else {
// Load from installed catalogs
@ -256,8 +228,7 @@ export function useFeaturedContent() {
// Sort by popular, newest, etc. (possibly enhanced later) and take first 10
const topItems = allItems.sort(() => Math.random() - 0.5).slice(0, 10);
// Optionally enrich with logos based on preference for tmdb-sourced IDs
const preference = settings.logoSourcePreference || 'tmdb';
// Optionally enrich with logos (TMDB only) for tmdb/imdb sourced IDs
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
const enrichLogo = async (item: any): Promise<StreamingContent> => {
@ -298,8 +269,8 @@ export function useFeaturedContent() {
tmdbId = found ? String(found) : null;
}
if (!tmdbId && !imdbId) return base;
// Only try TMDB if preference is 'tmdb' and we have tmdbId
if (preference === 'tmdb' && tmdbId) {
// Try TMDB if we have a TMDB id
if (tmdbId) {
logger.debug('[useFeaturedContent] logo:try:tmdb', { name: item.name, id: item.id, tmdbId, lang: preferredLanguage });
const logoUrl = await tmdbService.getContentLogo(item.type === 'series' ? 'tv' : 'movie', tmdbId as string, preferredLanguage);
if (logoUrl) {
@ -314,17 +285,29 @@ export function useFeaturedContent() {
}
};
// When enrichment is disabled, only use addon logos and never fetch external logos
if (!settings.enrichMetadataWithTMDB) {
logger.debug('[useFeaturedContent] enrichment disabled, using only addon logos');
formattedContent = topItems.map((item: any) => {
// Only enrich with logos if enrichment is enabled
if (settings.enrichMetadataWithTMDB) {
formattedContent = await Promise.all(topItems.map(enrichLogo));
try {
const details = formattedContent.slice(0, 20).map((c) => ({
id: c.id,
name: c.name,
hasLogo: Boolean(c.logo),
logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none',
logo: c.logo || undefined,
}));
logger.debug('[useFeaturedContent] catalogs:logos:details', { items: details });
} catch {}
} else {
// When enrichment is disabled, prefer addon-provided logos; if missing, fetch basic meta to pull logo (like HeroSection)
const baseItems = topItems.map((item: any) => {
const base: StreamingContent = {
id: item.id,
type: item.type,
name: item.name,
poster: item.poster,
banner: (item as any).banner,
logo: (item as any).logo && !isTmdbUrl((item as any).logo) ? (item as any).logo : undefined,
logo: (item as any).logo || undefined,
description: (item as any).description,
year: (item as any).year,
genres: (item as any).genres,
@ -332,9 +315,49 @@ export function useFeaturedContent() {
};
return base;
});
} else {
// Only enrich with logos if enrichment is enabled
formattedContent = await Promise.all(topItems.map(enrichLogo));
// Attempt to fill missing logos from addon meta details for a limited subset
const candidates = baseItems.filter(i => !i.logo).slice(0, 10);
logger.debug('[useFeaturedContent] catalogs:no-enrich:missing-logos', { count: candidates.length });
try {
const filled = await Promise.allSettled(candidates.map(async (item) => {
try {
const meta = await catalogService.getBasicContentDetails(item.type, item.id);
if (meta?.logo) {
logger.debug('[useFeaturedContent] catalogs:no-enrich:filled-logo', { id: item.id, name: item.name, logo: meta.logo });
return { id: item.id, logo: meta.logo } as { id: string; logo: string };
}
} catch (e) {
logger.warn('[useFeaturedContent] catalogs:no-enrich:fill-failed', { id: item.id, error: String(e) });
}
return { id: item.id, logo: undefined as any };
}));
const idToLogo = new Map<string, string>();
filled.forEach(res => {
if (res.status === 'fulfilled' && res.value && res.value.logo) {
idToLogo.set(res.value.id, res.value.logo);
}
});
formattedContent = baseItems.map(i => (
idToLogo.has(i.id) ? { ...i, logo: idToLogo.get(i.id)! } : i
));
} catch {
formattedContent = baseItems;
}
try {
const details = formattedContent.slice(0, 20).map((c) => ({
id: c.id,
name: c.name,
hasLogo: Boolean(c.logo),
logoSource: c.logo ? (isTmdbUrl(String(c.logo)) ? 'tmdb' : 'addon') : 'none',
logo: c.logo || undefined,
}));
logger.debug('[useFeaturedContent] catalogs:logos:details (no-enrich)', { items: details });
} catch {}
}
}
}

View file

@ -115,7 +115,6 @@ export const useLibrary = () => {
// Subscribe to catalogService library updates
useEffect(() => {
const unsubscribe = catalogService.subscribeToLibraryUpdates((items) => {
if (__DEV__) console.log('[useLibrary] Received library update from catalogService:', items.length, 'items');
setLibraryItems(items);
setLoading(false);
});

View file

@ -40,7 +40,6 @@ import HomeScreenSettings from '../screens/HomeScreenSettings';
import HeroCatalogsScreen from '../screens/HeroCatalogsScreen';
import TraktSettingsScreen from '../screens/TraktSettingsScreen';
import PlayerSettingsScreen from '../screens/PlayerSettingsScreen';
import LogoSourceSettings from '../screens/LogoSourceSettings';
import ThemeScreen from '../screens/ThemeScreen';
import OnboardingScreen from '../screens/OnboardingScreen';
import AuthScreen from '../screens/AuthScreen';
@ -135,7 +134,6 @@ export type RootStackParamList = {
HeroCatalogs: undefined;
TraktSettings: undefined;
PlayerSettings: undefined;
LogoSourceSettings: undefined;
ThemeSettings: undefined;
ScraperSettings: undefined;
CastMovies: {
@ -1239,21 +1237,6 @@ const InnerNavigator = ({ initialRouteName }: { initialRouteName?: keyof RootSta
},
}}
/>
<Stack.Screen
name="LogoSourceSettings"
component={LogoSourceSettings}
options={{
animation: Platform.OS === 'android' ? 'slide_from_right' : 'fade',
animationDuration: Platform.OS === 'android' ? 250 : 200,
presentation: 'card',
gestureEnabled: true,
gestureDirection: 'horizontal',
headerShown: false,
contentStyle: {
backgroundColor: currentTheme.colors.darkBackground,
},
}}
/>
<Stack.Screen
name="ThemeSettings"
component={ThemeScreen}

View file

@ -1,908 +0,0 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
StyleSheet,
TouchableOpacity,
ScrollView,
Switch,
SafeAreaView,
Image,
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';
import CustomAlert from '../components/CustomAlert';
// TMDB API key - since the default key might be private in the service, we'll use our own
const TMDB_API_KEY = '439c478a771f35c05022f9feabcca01c';
// Extra TMDB logo languages to always offer (only Arabic per request)
const COMMON_TMDB_LANGUAGES: string[] = ['ar'];
// 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<NavigationProp<any>>();
const insets = useSafeAreaInsets();
const { currentTheme } = useTheme();
const colors = currentTheme.colors;
const styles = createStyles(colors);
// CustomAlert state
const [alertVisible, setAlertVisible] = useState(false);
const [alertTitle, setAlertTitle] = useState('');
const [alertMessage, setAlertMessage] = useState('');
const [alertActions, setAlertActions] = useState<Array<{ label: string; onPress: () => void; style?: object }>>([
{ label: 'OK', onPress: () => setAlertVisible(false) },
]);
const openAlert = (
title: string,
message: string,
actions?: Array<{ label: string; onPress?: () => void; style?: object }>
) => {
setAlertTitle(title);
setAlertMessage(message);
if (actions && actions.length > 0) {
setAlertActions(
actions.map(a => ({
label: a.label,
style: a.style,
onPress: () => { a.onPress?.(); },
}))
);
} else {
setAlertActions([{ label: 'OK', onPress: () => setAlertVisible(false) }]);
}
setAlertVisible(true);
};
// 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<string | null>(null);
const [metahubLogo, setMetahubLogo] = useState<string | null>(null);
const [tmdbBanner, setTmdbBanner] = useState<string | null>(null);
const [metahubBanner, setMetahubBanner] = useState<string | null>(null);
const [loadingLogos, setLoadingLogos] = useState(true);
// Track which language the preview is actually using and if it is a fallback
const [previewLanguage, setPreviewLanguage] = useState<string>('');
const [isPreviewFallback, setIsPreviewFallback] = useState<boolean>(false);
// State for TMDB language selection
// Store unique language codes as strings
const [uniqueTmdbLanguages, setUniqueTmdbLanguages] = useState<string[]>([]);
const [tmdbLogosData, setTmdbLogosData] = useState<Array<{ iso_639_1: string; file_path: string }> | 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<string>(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}`);
setIsPreviewFallback(false);
} 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}`);
setIsPreviewFallback(true);
} 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}`);
setIsPreviewFallback(true);
}
}
if (initialLogoPath) {
setTmdbLogo(`https://image.tmdb.org/t/p/original${initialLogoPath}`);
setPreviewLanguage(initialLanguage || '');
} 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
setPreviewLanguage('');
setIsPreviewFallback(false);
}
// 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);
}
} 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
openAlert(
'Settings Updated',
`Logo and background source preference set to ${source === 'metahub' ? 'Metahub' : 'TMDB'}. Changes will apply when you navigate to content.`
);
};
// 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}`);
setPreviewLanguage(languageCode);
setIsPreviewFallback(false);
} else {
logger.warn(`[LogoSourceSettings] Could not find logo data for selected language: ${languageCode}`);
// Fallback to English, then first available if English is not present
const englishData = tmdbLogosData.find(logo => logo.iso_639_1 === 'en');
if (englishData) {
setTmdbLogo(`https://image.tmdb.org/t/p/original${englishData.file_path}`);
setPreviewLanguage('en');
setIsPreviewFallback(true);
} else if (tmdbLogosData[0]) {
setTmdbLogo(`https://image.tmdb.org/t/p/original${tmdbLogosData[0].file_path}`);
setPreviewLanguage(tmdbLogosData[0].iso_639_1 || '');
setIsPreviewFallback(true);
} else {
setPreviewLanguage('');
setIsPreviewFallback(false);
}
}
}
// 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
openAlert(
'TMDB Language Updated',
`TMDB logo language preference set to ${languageCode.toUpperCase()}. Changes will apply when you navigate to content.`
);
} catch (e) {
logger.error(`[LogoSourceSettings] Error in saveLanguagePreference:`, e);
// Show error notification
openAlert(
'Error Saving Preference',
'There was a problem saving your language preference. Please try again.'
);
}
};
// 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 (
<View style={[styles.exampleImage, styles.loadingContainer]}>
<ActivityIndicator size="small" color={colors.primary} />
</View>
);
}
return (
<View style={styles.bannerContainer}>
<Image
source={{ uri: banner || undefined }}
style={styles.bannerImage}
resizeMode="cover"
/>
<View style={styles.bannerOverlay} />
{logo && (
<Image
source={{ uri: logo }}
style={styles.logoOverBanner}
resizeMode="contain"
/>
)}
{!logo && (
<View style={styles.noLogoContainer}>
<Text style={styles.noLogoText}>No logo available</Text>
</View>
)}
</View>
);
};
return (
<SafeAreaView style={[styles.container]}>
<StatusBar barStyle="light-content" />
{/* Header */}
<View style={styles.header}>
<TouchableOpacity
onPress={handleBack}
style={styles.backButton}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<MaterialIcons name="arrow-back" size={24} color={colors.white} />
<Text style={styles.backText}>Settings</Text>
</TouchableOpacity>
<View style={styles.headerActions}>
{/* Empty for now, but ready for future actions */}
</View>
</View>
<Text style={styles.headerTitle}>Logo Source</Text>
<ScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={true}
scrollEventThrottle={32}
decelerationRate="normal"
>
{/* Description */}
<View style={styles.descriptionContainer}>
<Text style={styles.description}>
Choose the primary source for content logos and backgrounds. The selected source will be used exclusively.
</Text>
</View>
{/* Show selector */}
<View style={styles.showSelectorContainer}>
<Text style={styles.selectorLabel}>Select a show/movie to preview:</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.showsScrollContent}
scrollEventThrottle={32}
decelerationRate="normal"
>
{EXAMPLE_SHOWS.map((show) => (
<TouchableOpacity
key={show.imdbId}
style={[
styles.showItem,
selectedShow.imdbId === show.imdbId && styles.selectedShowItem
]}
onPress={() => handleShowSelect(show)}
activeOpacity={0.7}
delayPressIn={100}
>
<Text
style={[
styles.showItemText,
selectedShow.imdbId === show.imdbId && styles.selectedShowItemText
]}
>
{show.name}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
{/* Options */}
<View style={styles.optionsContainer}>
<TouchableOpacity
style={[
styles.optionCard,
logoSource === 'metahub' && styles.selectedCard
]}
onPress={() => applyLogoSourceSetting('metahub')}
activeOpacity={0.7}
delayPressIn={100}
>
<View style={styles.optionHeader}>
<Text style={styles.optionTitle}>Metahub</Text>
{logoSource === 'metahub' && (
<MaterialIcons name="check-circle" size={24} color={colors.primary} />
)}
</View>
<Text style={styles.optionDescription}>
High-quality logos from Metahub. Best for popular titles.
</Text>
<View style={styles.exampleContainer}>
<Text style={styles.exampleLabel}>Example:</Text>
{renderLogoExample(metahubLogo, metahubBanner, loadingLogos)}
<Text style={styles.logoSourceLabel}>{selectedShow.name} logo from Metahub</Text>
</View>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.optionCard,
logoSource === 'tmdb' && styles.selectedCard
]}
onPress={() => applyLogoSourceSetting('tmdb')}
activeOpacity={0.7}
delayPressIn={100}
>
<View style={styles.optionHeader}>
<Text style={styles.optionTitle}>TMDB</Text>
{logoSource === 'tmdb' && (
<MaterialIcons name="check-circle" size={24} color={colors.primary} />
)}
</View>
<Text style={styles.optionDescription}>
Logos from TMDB. Offers localized options and better coverage for recent content.
</Text>
<View style={styles.exampleContainer}>
<Text style={styles.exampleLabel}>Example:</Text>
{renderLogoExample(tmdbLogo, tmdbBanner, loadingLogos)}
<Text style={styles.logoSourceLabel}>
{`Preview language: ${(previewLanguage || '').toUpperCase() || 'N/A'}${isPreviewFallback ? ' (fallback)' : ''}`}
</Text>
<Text style={styles.logoSourceLabel}>{selectedShow.name} logo from TMDB</Text>
</View>
{/* TMDB Language Selector */}
{true && (
<View style={styles.languageSelectorContainer}>
<Text style={styles.languageSelectorTitle}>Logo Language</Text>
<Text style={styles.languageSelectorDescription}>
Select your preferred language for TMDB logos (includes common languages like Arabic even if not shown in this preview).
</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.languageScrollContent}
scrollEventThrottle={32}
decelerationRate="normal"
>
{/* Merge unique languages from TMDB with a common list to ensure wider options */}
{Array.from(new Set<string>([...uniqueTmdbLanguages, ...COMMON_TMDB_LANGUAGES])).map((langCode) => (
<TouchableOpacity
key={langCode} // Use the unique code as key
style={[
styles.languageItem,
preferredTmdbLanguage === langCode && styles.selectedLanguageItem
]}
onPress={() => handleTmdbLanguageSelect(langCode)}
activeOpacity={0.7}
delayPressIn={150}
>
<Text
style={[
styles.languageItemText,
preferredTmdbLanguage === langCode && styles.selectedLanguageItemText
]}
>
{(langCode || '').toUpperCase() || '??'}
</Text>
</TouchableOpacity>
))}
</ScrollView>
<Text style={styles.noteText}>
If unavailable in preferred language, English will be used as fallback.
</Text>
</View>
)}
</TouchableOpacity>
</View>
{/* Additional Info */}
<View style={styles.infoBox}>
<Text style={styles.infoText}>
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.
</Text>
</View>
</ScrollView>
<CustomAlert
visible={alertVisible}
title={alertTitle}
message={alertMessage}
onClose={() => setAlertVisible(false)}
actions={alertActions}
/>
</SafeAreaView>
);
};
export default LogoSourceSettings;

View file

@ -557,18 +557,10 @@ const SettingsScreen: React.FC = () => {
/>
<SettingItem
title="TMDB"
description="Metadata provider"
description="Metadata & logo source provider"
icon="movie"
renderControl={ChevronRight}
onPress={() => navigation.navigate('TMDBSettings')}
isTablet={isTablet}
/>
<SettingItem
title="Media Sources"
description="Logo & image preferences"
icon="image"
renderControl={ChevronRight}
onPress={() => navigation.navigate('LogoSourceSettings')}
isLast={true}
isTablet={isTablet}
/>

View file

@ -32,6 +32,35 @@ import CustomAlert from '../components/CustomAlert';
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key';
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: 'Stranger Things',
imdbId: 'tt4574334',
tmdbId: '66732',
type: 'tv' as const
},
{
name: 'Avatar',
imdbId: 'tt0499549',
tmdbId: '19995',
type: 'movie' as const
},
];
const TMDBSettingsScreen = () => {
const navigation = useNavigation();
@ -53,6 +82,14 @@ const TMDBSettingsScreen = () => {
const { settings, updateSetting } = useSettings();
const [languagePickerVisible, setLanguagePickerVisible] = useState(false);
const [languageSearch, setLanguageSearch] = useState('');
// Logo preview state
const [selectedShow, setSelectedShow] = useState(EXAMPLE_SHOWS[0]);
const [tmdbLogo, setTmdbLogo] = useState<string | null>(null);
const [tmdbBanner, setTmdbBanner] = useState<string | null>(null);
const [loadingLogos, setLoadingLogos] = useState(true);
const [previewLanguage, setPreviewLanguage] = useState<string>('');
const [isPreviewFallback, setIsPreviewFallback] = useState<boolean>(false);
const openAlert = (
title: string,
@ -253,6 +290,151 @@ const TMDBSettingsScreen = () => {
});
};
// Logo preview functions
const fetchExampleLogos = async (show: typeof EXAMPLE_SHOWS[0]) => {
setLoadingLogos(true);
setTmdbLogo(null);
setTmdbBanner(null);
try {
const tmdbId = show.tmdbId;
const contentType = show.type;
logger.log(`[TMDBSettingsScreen] Fetching ${show.name} with TMDB ID: ${tmdbId}`);
const preferredTmdbLanguage = settings.tmdbLanguagePreference || 'en';
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();
if (imagesData.logos && imagesData.logos.length > 0) {
let logoPath: string | null = null;
let logoLanguage = preferredTmdbLanguage;
// Try to find logo in preferred language
const preferredLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === preferredTmdbLanguage);
if (preferredLogo) {
logoPath = preferredLogo.file_path;
logoLanguage = preferredTmdbLanguage;
setIsPreviewFallback(false);
} else {
// Fallback to English
const englishLogo = imagesData.logos.find((logo: { iso_639_1: string; file_path: string }) => logo.iso_639_1 === 'en');
if (englishLogo) {
logoPath = englishLogo.file_path;
logoLanguage = 'en';
setIsPreviewFallback(true);
} else if (imagesData.logos[0]) {
// Fallback to first available
logoPath = imagesData.logos[0].file_path;
logoLanguage = imagesData.logos[0].iso_639_1 || 'unknown';
setIsPreviewFallback(true);
}
}
if (logoPath) {
setTmdbLogo(`https://image.tmdb.org/t/p/original${logoPath}`);
setPreviewLanguage(logoLanguage);
} else {
setPreviewLanguage('');
setIsPreviewFallback(false);
}
} else {
setPreviewLanguage('');
setIsPreviewFallback(false);
}
// Get TMDB banner (backdrop)
if (imagesData.backdrops && imagesData.backdrops.length > 0) {
const backdropPath = imagesData.backdrops[0].file_path;
setTmdbBanner(`https://image.tmdb.org/t/p/original${backdropPath}`);
} else {
const detailsResponse = await fetch(`https://api.themoviedb.org/3/${endpoint}/${tmdbId}?api_key=${apiKey}`);
const details = await detailsResponse.json();
if (details.backdrop_path) {
setTmdbBanner(`https://image.tmdb.org/t/p/original${details.backdrop_path}`);
}
}
} catch (err) {
logger.error(`[TMDBSettingsScreen] Error fetching ${show.name} preview:`, err);
} finally {
setLoadingLogos(false);
}
};
const handleShowSelect = (show: typeof EXAMPLE_SHOWS[0]) => {
setSelectedShow(show);
try {
AsyncStorage.setItem('tmdb_settings_selected_show', show.imdbId);
} catch (e) {
if (__DEV__) console.error('Error saving selected show:', e);
}
};
const renderLogoExample = (logo: string | null, banner: string | null, isLoading: boolean) => {
if (isLoading) {
return (
<View style={[styles.exampleImage, styles.loadingContainer]}>
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
</View>
);
}
return (
<View style={styles.bannerContainer}>
<Image
source={{ uri: banner || undefined }}
style={styles.bannerImage}
resizeMode="cover"
/>
<View style={styles.bannerOverlay} />
{logo && (
<Image
source={{ uri: logo }}
style={styles.logoOverBanner}
resizeMode="contain"
/>
)}
{!logo && (
<View style={styles.noLogoContainer}>
<Text style={styles.noLogoText}>No logo available</Text>
</View>
)}
</View>
);
};
// Load example logos when show or language changes
useEffect(() => {
if (settings.enrichMetadataWithTMDB && settings.useTmdbLocalizedMetadata) {
fetchExampleLogos(selectedShow);
}
}, [selectedShow, settings.enrichMetadataWithTMDB, settings.useTmdbLocalizedMetadata, settings.tmdbLanguagePreference]);
// Load selected show from AsyncStorage on mount
useEffect(() => {
const loadSelectedShow = async () => {
try {
const savedShowId = await AsyncStorage.getItem('tmdb_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();
}, []);
const headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
const headerHeight = headerBaseHeight + topSpacing;
@ -357,6 +539,56 @@ const TMDBSettingsScreen = () => {
<Text style={[styles.languageButtonText, { color: currentTheme.colors.white }]}>Change</Text>
</TouchableOpacity>
</View>
{/* Logo Preview */}
<View style={styles.divider} />
<Text style={[styles.settingTitle, { color: currentTheme.colors.text, marginBottom: 8 }]}>Logo Preview</Text>
<Text style={[styles.settingDescription, { color: currentTheme.colors.mediumEmphasis, marginBottom: 12 }]}>
Preview shows how localized logos will appear in the selected language.
</Text>
{/* Show selector */}
<Text style={[styles.selectorLabel, { color: currentTheme.colors.mediumEmphasis }]}>Example:</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.showsScrollContent}
style={styles.showsScrollView}
>
{EXAMPLE_SHOWS.map((show) => (
<TouchableOpacity
key={show.imdbId}
style={[
styles.showItem,
{ backgroundColor: currentTheme.colors.elevation1 },
selectedShow.imdbId === show.imdbId && [styles.selectedShowItem, { borderColor: currentTheme.colors.primary }]
]}
onPress={() => handleShowSelect(show)}
activeOpacity={0.7}
>
<Text
style={[
styles.showItemText,
{ color: currentTheme.colors.mediumEmphasis },
selectedShow.imdbId === show.imdbId && [styles.selectedShowItemText, { color: currentTheme.colors.white }]
]}
>
{show.name}
</Text>
</TouchableOpacity>
))}
</ScrollView>
{/* Preview card */}
<View style={[styles.logoPreviewCard, { backgroundColor: currentTheme.colors.elevation1 }]}>
{renderLogoExample(tmdbLogo, tmdbBanner, loadingLogos)}
{tmdbLogo && (
<Text style={[styles.logoSourceLabel, { color: currentTheme.colors.mediumEmphasis }]}>
{`Language: ${(previewLanguage || '').toUpperCase() || 'N/A'}${isPreviewFallback ? ' (fallback to available)' : ''}`}
</Text>
)}
</View>
</>
)}
</>
@ -1113,6 +1345,91 @@ const styles = StyleSheet.create({
fontSize: 16,
fontWeight: '700',
},
// Logo Source Styles
selectorLabel: {
fontSize: 13,
marginBottom: 8,
marginTop: 4,
},
showsScrollView: {
marginBottom: 16,
},
showsScrollContent: {
paddingRight: 16,
paddingVertical: 2,
},
showItem: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 16,
marginRight: 6,
borderWidth: 1,
borderColor: 'transparent',
},
selectedShowItem: {
borderWidth: 2,
},
showItemText: {
fontSize: 13,
},
selectedShowItemText: {
fontWeight: '600',
},
logoPreviewCard: {
borderRadius: 12,
padding: 12,
marginTop: 12,
},
exampleImage: {
height: 60,
width: '100%',
backgroundColor: 'rgba(0,0,0,0.5)',
borderRadius: 8,
},
bannerContainer: {
height: 80,
width: '100%',
borderRadius: 8,
overflow: 'hidden',
position: 'relative',
marginTop: 4,
},
bannerImage: {
...StyleSheet.absoluteFillObject,
},
bannerOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.4)',
},
logoOverBanner: {
position: 'absolute',
width: '80%',
height: '70%',
alignSelf: 'center',
top: '15%',
},
noLogoContainer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
},
noLogoText: {
color: '#fff',
fontSize: 13,
backgroundColor: 'rgba(0,0,0,0.5)',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 4,
},
logoSourceLabel: {
fontSize: 11,
marginTop: 6,
},
});
export default TMDBSettingsScreen;

View file

@ -588,6 +588,14 @@ class CatalogService {
if (!logoUrl || logoUrl.trim() === '' || logoUrl === 'null' || logoUrl === 'undefined') {
logoUrl = undefined;
}
try {
logger.debug('[CatalogService] convertMetaToStreamingContent:logo', {
id: meta.id,
name: meta.name,
hasLogo: Boolean(logoUrl),
logo: logoUrl || undefined,
});
} catch {}
return {
id: meta.id,

View file

@ -49,7 +49,6 @@ class RobustCalendarCache {
return null;
}
logger.log(`[Cache] Valid cache found for key ${key}`);
return cache.data;
} catch (error) {
logger.error(`[Cache] Error getting cached data for key ${key}:`, error);