mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +00:00
some tmdb logo fetching logic changes
This commit is contained in:
parent
42c236e235
commit
238f08192f
10 changed files with 425 additions and 1016 deletions
|
|
@ -253,7 +253,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
||||||
setLogoLoadError(false);
|
setLogoLoadError(false);
|
||||||
}, [featuredContent?.id]);
|
}, [featuredContent?.id]);
|
||||||
|
|
||||||
// Fetch logo based on preference
|
// Fetch logo when enrichment is enabled; otherwise only use addon logo
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!featuredContent || logoFetchInProgress.current) return;
|
if (!featuredContent || logoFetchInProgress.current) return;
|
||||||
|
|
||||||
|
|
@ -267,8 +267,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
||||||
const contentData = featuredContent; // Use a clearer variable name
|
const contentData = featuredContent; // Use a clearer variable name
|
||||||
const currentLogo = contentData.logo;
|
const currentLogo = contentData.logo;
|
||||||
|
|
||||||
// Get preferences
|
// Get language preference (only relevant when enrichment is enabled)
|
||||||
const logoPreference = settings.logoSourcePreference || 'tmdb';
|
|
||||||
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
|
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
|
||||||
|
|
||||||
// If enrichment is disabled, use addon logo and don't fetch from external sources
|
// 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 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 });
|
logger.info('[FeaturedContent] enrichment disabled, using addon logo', { logo: contentData.logo });
|
||||||
setLogoUrl(contentData.logo);
|
setLogoUrl(contentData.logo);
|
||||||
logoFetchInProgress.current = false;
|
logoFetchInProgress.current = false;
|
||||||
|
|
@ -334,11 +333,11 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
||||||
let primaryAttempted = false;
|
let primaryAttempted = false;
|
||||||
let fallbackAttempted = false;
|
let fallbackAttempted = false;
|
||||||
|
|
||||||
// --- Logo Fetching Logic ---
|
// --- Logo Fetching Logic (TMDB only when enrichment is enabled) ---
|
||||||
logger.debug('[FeaturedContent] fetchLogo:ids', { imdbId, tmdbId, preference: logoPreference, lang: preferredLanguage });
|
logger.debug('[FeaturedContent] fetchLogo:ids', { imdbId, tmdbId, lang: preferredLanguage });
|
||||||
|
|
||||||
// Only try TMDB if preference is 'tmdb' and we have tmdbId
|
// Try TMDB if we have a TMDB id
|
||||||
if (logoPreference === 'tmdb' && tmdbId) {
|
if (tmdbId) {
|
||||||
primaryAttempted = true;
|
primaryAttempted = true;
|
||||||
try {
|
try {
|
||||||
const tmdbService = TMDBService.getInstance();
|
const tmdbService = TMDBService.getInstance();
|
||||||
|
|
@ -354,11 +353,11 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
||||||
// --- Set Final Logo ---
|
// --- Set Final Logo ---
|
||||||
if (finalLogoUrl) {
|
if (finalLogoUrl) {
|
||||||
setLogoUrl(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) {
|
} else if (currentLogo) {
|
||||||
// Use existing logo only if primary and fallback failed or weren't applicable
|
// Use existing logo only if primary and fallback failed or weren't applicable
|
||||||
setLogoUrl(currentLogo);
|
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 {
|
} else {
|
||||||
// No logo found from any source
|
// No logo found from any source
|
||||||
setLogoLoadError(true);
|
setLogoLoadError(true);
|
||||||
|
|
@ -377,7 +376,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
|
||||||
|
|
||||||
// Trigger fetch when content changes
|
// Trigger fetch when content changes
|
||||||
fetchLogo();
|
fetchLogo();
|
||||||
}, [featuredContent, settings.logoSourcePreference, settings.tmdbLanguagePreference, settings.enrichMetadataWithTMDB]);
|
}, [featuredContent, settings.tmdbLanguagePreference, settings.enrichMetadataWithTMDB]);
|
||||||
|
|
||||||
// Load poster and logo
|
// Load poster and logo
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
||||||
} = useTraktContext();
|
} = useTraktContext();
|
||||||
|
|
||||||
const fetchCalendarData = useCallback(async (forceRefresh = false) => {
|
const fetchCalendarData = useCallback(async (forceRefresh = false) => {
|
||||||
logger.log("[CalendarData] Starting to fetch calendar data");
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -68,14 +67,12 @@ export const useCalendarData = (): UseCalendarDataReturn => {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cachedData) {
|
if (cachedData) {
|
||||||
logger.log(`[CalendarData] Using cached data with ${cachedData.length} sections`);
|
|
||||||
setCalendarData(cachedData);
|
setCalendarData(cachedData);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log("[CalendarData] Fetching fresh data from APIs");
|
|
||||||
|
|
||||||
const librarySeries = libraryItems.filter(item => item.type === 'series');
|
const librarySeries = libraryItems.filter(item => item.type === 'series');
|
||||||
let allSeries: StreamingContent[] = [...librarySeries];
|
let allSeries: StreamingContent[] = [...librarySeries];
|
||||||
|
|
|
||||||
|
|
@ -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 tLogos = Date.now();
|
||||||
const preference = settings.logoSourcePreference || 'tmdb';
|
|
||||||
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
|
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
|
||||||
|
|
||||||
const fetchLogoForItem = async (item: StreamingContent): Promise<StreamingContent> => {
|
const fetchLogoForItem = async (item: StreamingContent): Promise<StreamingContent> => {
|
||||||
|
|
@ -152,58 +151,21 @@ export function useFeaturedContent() {
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preference === 'tmdb') {
|
// Enrichment path: TMDB only
|
||||||
logger.debug('[useFeaturedContent] logo:try:tmdb', { name: item.name, id: item.id, tmdbId, lang: preferredLanguage });
|
logger.debug('[useFeaturedContent] logo:try:tmdb', { name: item.name, id: item.id, tmdbId, lang: preferredLanguage });
|
||||||
// Resolve TMDB id if we only have IMDb
|
// Resolve TMDB id if we only have IMDb
|
||||||
if (!tmdbId && imdbId) {
|
if (!tmdbId && imdbId) {
|
||||||
const found = await tmdbService.findTMDBIdByIMDB(imdbId);
|
const found = await tmdbService.findTMDBIdByIMDB(imdbId);
|
||||||
tmdbId = found ? String(found) : null;
|
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;
|
|
||||||
}
|
}
|
||||||
|
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) {
|
} catch (error) {
|
||||||
logger.error('[useFeaturedContent] logo:error', { name: item.name, id: item.id, error: String(error) });
|
logger.error('[useFeaturedContent] logo:error', { name: item.name, id: item.id, error: String(error) });
|
||||||
return item;
|
return item;
|
||||||
|
|
@ -220,7 +182,17 @@ export function useFeaturedContent() {
|
||||||
logo: item.logo && !isTmdbUrl(item.logo) ? item.logo : undefined
|
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 {
|
} else {
|
||||||
// Load from installed catalogs
|
// Load from installed catalogs
|
||||||
|
|
@ -256,8 +228,7 @@ export function useFeaturedContent() {
|
||||||
// Sort by popular, newest, etc. (possibly enhanced later) and take first 10
|
// Sort by popular, newest, etc. (possibly enhanced later) and take first 10
|
||||||
const topItems = allItems.sort(() => Math.random() - 0.5).slice(0, 10);
|
const topItems = allItems.sort(() => Math.random() - 0.5).slice(0, 10);
|
||||||
|
|
||||||
// Optionally enrich with logos based on preference for tmdb-sourced IDs
|
// Optionally enrich with logos (TMDB only) for tmdb/imdb sourced IDs
|
||||||
const preference = settings.logoSourcePreference || 'tmdb';
|
|
||||||
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
|
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
|
||||||
|
|
||||||
const enrichLogo = async (item: any): Promise<StreamingContent> => {
|
const enrichLogo = async (item: any): Promise<StreamingContent> => {
|
||||||
|
|
@ -298,8 +269,8 @@ export function useFeaturedContent() {
|
||||||
tmdbId = found ? String(found) : null;
|
tmdbId = found ? String(found) : null;
|
||||||
}
|
}
|
||||||
if (!tmdbId && !imdbId) return base;
|
if (!tmdbId && !imdbId) return base;
|
||||||
// Only try TMDB if preference is 'tmdb' and we have tmdbId
|
// Try TMDB if we have a TMDB id
|
||||||
if (preference === 'tmdb' && tmdbId) {
|
if (tmdbId) {
|
||||||
logger.debug('[useFeaturedContent] logo:try:tmdb', { name: item.name, id: item.id, tmdbId, lang: preferredLanguage });
|
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);
|
const logoUrl = await tmdbService.getContentLogo(item.type === 'series' ? 'tv' : 'movie', tmdbId as string, preferredLanguage);
|
||||||
if (logoUrl) {
|
if (logoUrl) {
|
||||||
|
|
@ -314,17 +285,29 @@ export function useFeaturedContent() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// When enrichment is disabled, only use addon logos and never fetch external logos
|
// Only enrich with logos if enrichment is enabled
|
||||||
if (!settings.enrichMetadataWithTMDB) {
|
if (settings.enrichMetadataWithTMDB) {
|
||||||
logger.debug('[useFeaturedContent] enrichment disabled, using only addon logos');
|
formattedContent = await Promise.all(topItems.map(enrichLogo));
|
||||||
formattedContent = topItems.map((item: any) => {
|
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 = {
|
const base: StreamingContent = {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
type: item.type,
|
type: item.type,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
poster: item.poster,
|
poster: item.poster,
|
||||||
banner: (item as any).banner,
|
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,
|
description: (item as any).description,
|
||||||
year: (item as any).year,
|
year: (item as any).year,
|
||||||
genres: (item as any).genres,
|
genres: (item as any).genres,
|
||||||
|
|
@ -332,9 +315,49 @@ export function useFeaturedContent() {
|
||||||
};
|
};
|
||||||
return base;
|
return base;
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// Only enrich with logos if enrichment is enabled
|
// Attempt to fill missing logos from addon meta details for a limited subset
|
||||||
formattedContent = await Promise.all(topItems.map(enrichLogo));
|
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 {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -115,7 +115,6 @@ export const useLibrary = () => {
|
||||||
// Subscribe to catalogService library updates
|
// Subscribe to catalogService library updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribe = catalogService.subscribeToLibraryUpdates((items) => {
|
const unsubscribe = catalogService.subscribeToLibraryUpdates((items) => {
|
||||||
if (__DEV__) console.log('[useLibrary] Received library update from catalogService:', items.length, 'items');
|
|
||||||
setLibraryItems(items);
|
setLibraryItems(items);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,6 @@ import HomeScreenSettings from '../screens/HomeScreenSettings';
|
||||||
import HeroCatalogsScreen from '../screens/HeroCatalogsScreen';
|
import HeroCatalogsScreen from '../screens/HeroCatalogsScreen';
|
||||||
import TraktSettingsScreen from '../screens/TraktSettingsScreen';
|
import TraktSettingsScreen from '../screens/TraktSettingsScreen';
|
||||||
import PlayerSettingsScreen from '../screens/PlayerSettingsScreen';
|
import PlayerSettingsScreen from '../screens/PlayerSettingsScreen';
|
||||||
import LogoSourceSettings from '../screens/LogoSourceSettings';
|
|
||||||
import ThemeScreen from '../screens/ThemeScreen';
|
import ThemeScreen from '../screens/ThemeScreen';
|
||||||
import OnboardingScreen from '../screens/OnboardingScreen';
|
import OnboardingScreen from '../screens/OnboardingScreen';
|
||||||
import AuthScreen from '../screens/AuthScreen';
|
import AuthScreen from '../screens/AuthScreen';
|
||||||
|
|
@ -135,7 +134,6 @@ export type RootStackParamList = {
|
||||||
HeroCatalogs: undefined;
|
HeroCatalogs: undefined;
|
||||||
TraktSettings: undefined;
|
TraktSettings: undefined;
|
||||||
PlayerSettings: undefined;
|
PlayerSettings: undefined;
|
||||||
LogoSourceSettings: undefined;
|
|
||||||
ThemeSettings: undefined;
|
ThemeSettings: undefined;
|
||||||
ScraperSettings: undefined;
|
ScraperSettings: undefined;
|
||||||
CastMovies: {
|
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
|
<Stack.Screen
|
||||||
name="ThemeSettings"
|
name="ThemeSettings"
|
||||||
component={ThemeScreen}
|
component={ThemeScreen}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -557,18 +557,10 @@ const SettingsScreen: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
<SettingItem
|
<SettingItem
|
||||||
title="TMDB"
|
title="TMDB"
|
||||||
description="Metadata provider"
|
description="Metadata & logo source provider"
|
||||||
icon="movie"
|
icon="movie"
|
||||||
renderControl={ChevronRight}
|
renderControl={ChevronRight}
|
||||||
onPress={() => navigation.navigate('TMDBSettings')}
|
onPress={() => navigation.navigate('TMDBSettings')}
|
||||||
isTablet={isTablet}
|
|
||||||
/>
|
|
||||||
<SettingItem
|
|
||||||
title="Media Sources"
|
|
||||||
description="Logo & image preferences"
|
|
||||||
icon="image"
|
|
||||||
renderControl={ChevronRight}
|
|
||||||
onPress={() => navigation.navigate('LogoSourceSettings')}
|
|
||||||
isLast={true}
|
isLast={true}
|
||||||
isTablet={isTablet}
|
isTablet={isTablet}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,35 @@ import CustomAlert from '../components/CustomAlert';
|
||||||
|
|
||||||
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
|
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
|
||||||
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_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 TMDBSettingsScreen = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|
@ -53,6 +82,14 @@ const TMDBSettingsScreen = () => {
|
||||||
const { settings, updateSetting } = useSettings();
|
const { settings, updateSetting } = useSettings();
|
||||||
const [languagePickerVisible, setLanguagePickerVisible] = useState(false);
|
const [languagePickerVisible, setLanguagePickerVisible] = useState(false);
|
||||||
const [languageSearch, setLanguageSearch] = useState('');
|
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 = (
|
const openAlert = (
|
||||||
title: string,
|
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 headerBaseHeight = Platform.OS === 'android' ? 80 : 60;
|
||||||
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
|
const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top;
|
||||||
const headerHeight = headerBaseHeight + topSpacing;
|
const headerHeight = headerBaseHeight + topSpacing;
|
||||||
|
|
@ -357,6 +539,56 @@ const TMDBSettingsScreen = () => {
|
||||||
<Text style={[styles.languageButtonText, { color: currentTheme.colors.white }]}>Change</Text>
|
<Text style={[styles.languageButtonText, { color: currentTheme.colors.white }]}>Change</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</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,
|
fontSize: 16,
|
||||||
fontWeight: '700',
|
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;
|
export default TMDBSettingsScreen;
|
||||||
|
|
@ -588,6 +588,14 @@ class CatalogService {
|
||||||
if (!logoUrl || logoUrl.trim() === '' || logoUrl === 'null' || logoUrl === 'undefined') {
|
if (!logoUrl || logoUrl.trim() === '' || logoUrl === 'null' || logoUrl === 'undefined') {
|
||||||
logoUrl = undefined;
|
logoUrl = undefined;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
logger.debug('[CatalogService] convertMetaToStreamingContent:logo', {
|
||||||
|
id: meta.id,
|
||||||
|
name: meta.name,
|
||||||
|
hasLogo: Boolean(logoUrl),
|
||||||
|
logo: logoUrl || undefined,
|
||||||
|
});
|
||||||
|
} catch {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: meta.id,
|
id: meta.id,
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ class RobustCalendarCache {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log(`[Cache] Valid cache found for key ${key}`);
|
|
||||||
return cache.data;
|
return cache.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[Cache] Error getting cached data for key ${key}:`, error);
|
logger.error(`[Cache] Error getting cached data for key ${key}:`, error);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue