mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
UI changes on herocarousal
This commit is contained in:
parent
8700b10843
commit
412d25c458
2 changed files with 203 additions and 31 deletions
|
|
@ -28,6 +28,7 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
|||
|
||||
const data = useMemo(() => (items && items.length ? items.slice(0, 10) : []), [items]);
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [failedLogoIds, setFailedLogoIds] = useState<Set<string>>(new Set());
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
|
@ -149,11 +150,24 @@ const HeroCarousel: React.FC<HeroCarouselProps> = ({ items, loading = false }) =
|
|||
/>
|
||||
</View>
|
||||
<View style={styles.info as ViewStyle}>
|
||||
<Text style={[styles.title as TextStyle, { color: currentTheme.colors.highEmphasis }]} numberOfLines={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
{item.logo && !failedLogoIds.has(item.id) ? (
|
||||
<ExpoImage
|
||||
source={{ uri: item.logo }}
|
||||
style={styles.logo as ImageStyle}
|
||||
contentFit="contain"
|
||||
transition={250}
|
||||
cachePolicy="memory-disk"
|
||||
onError={() => {
|
||||
setFailedLogoIds((prev) => new Set(prev).add(item.id));
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Text style={[styles.title as TextStyle, { color: currentTheme.colors.highEmphasis, textAlign: 'center' }]} numberOfLines={1}>
|
||||
{item.name}
|
||||
</Text>
|
||||
)}
|
||||
{item.genres && (
|
||||
<Text style={[styles.genres as TextStyle, { color: currentTheme.colors.mediumEmphasis }]} numberOfLines={1}>
|
||||
<Text style={[styles.genres as TextStyle, { color: currentTheme.colors.mediumEmphasis, textAlign: 'center' }]} numberOfLines={1}>
|
||||
{item.genres.slice(0, 3).join(' • ')}
|
||||
</Text>
|
||||
)}
|
||||
|
|
@ -297,6 +311,7 @@ const styles = StyleSheet.create({
|
|||
paddingHorizontal: 16,
|
||||
paddingTop: 10,
|
||||
paddingBottom: 12,
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
|
|
@ -311,6 +326,12 @@ const styles = StyleSheet.create({
|
|||
alignItems: 'center',
|
||||
gap: 10,
|
||||
marginTop: 12,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
logo: {
|
||||
width: Math.round(CARD_WIDTH * 0.72),
|
||||
height: 64,
|
||||
marginBottom: 6,
|
||||
},
|
||||
playButton: {
|
||||
flexDirection: 'row',
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ const persistentStore = {
|
|||
lastSettings: {
|
||||
showHeroSection: true,
|
||||
featuredContentSource: 'tmdb' as 'tmdb' | 'catalogs',
|
||||
selectedHeroCatalogs: [] as string[]
|
||||
selectedHeroCatalogs: [] as string[],
|
||||
logoSourcePreference: 'metahub' as 'metahub' | 'tmdb',
|
||||
tmdbLanguagePreference: 'en'
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -127,29 +129,87 @@ export function useFeaturedContent() {
|
|||
};
|
||||
});
|
||||
|
||||
// Then fetch logos for each item
|
||||
// Then fetch logos for each item based on preference
|
||||
const tLogos = Date.now();
|
||||
formattedContent = await Promise.all(
|
||||
preFormattedContent.map(async (item) => {
|
||||
try {
|
||||
if (item.id.startsWith('tmdb:')) {
|
||||
const tmdbId = item.id.split(':')[1];
|
||||
const logoUrl = await tmdbService.getContentLogo('movie', tmdbId);
|
||||
if (logoUrl) {
|
||||
return {
|
||||
...item,
|
||||
logo: logoUrl
|
||||
};
|
||||
}
|
||||
}
|
||||
return item;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to fetch logo for ${item.name}:`, error);
|
||||
const preference = settings.logoSourcePreference || 'metahub';
|
||||
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
|
||||
|
||||
const fetchLogoForItem = async (item: StreamingContent): Promise<StreamingContent> => {
|
||||
try {
|
||||
// Support both TMDB-prefixed and IMDb-prefixed IDs
|
||||
const isTmdb = item.id.startsWith('tmdb:');
|
||||
const isImdb = item.id.startsWith('tt');
|
||||
let tmdbId: string | null = null;
|
||||
let imdbId: string | null = null;
|
||||
|
||||
if (isTmdb) {
|
||||
tmdbId = item.id.split(':')[1];
|
||||
} else if (isImdb) {
|
||||
imdbId = item.id.split(':')[0];
|
||||
} else {
|
||||
return item;
|
||||
}
|
||||
})
|
||||
);
|
||||
logger.info('[useFeaturedContent] tmdb:logos', { count: formattedContent.length, duration: `${Date.now() - tLogos}ms` });
|
||||
|
||||
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;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[useFeaturedContent] logo:error', { name: item.name, id: item.id, error: String(error) });
|
||||
return item;
|
||||
}
|
||||
};
|
||||
|
||||
formattedContent = await Promise.all(preFormattedContent.map(fetchLogoForItem));
|
||||
logger.info('[useFeaturedContent] logos:resolved', { count: formattedContent.length, duration: `${Date.now() - tLogos}ms`, preference });
|
||||
}
|
||||
} else {
|
||||
// Load from installed catalogs
|
||||
|
|
@ -182,8 +242,86 @@ export function useFeaturedContent() {
|
|||
);
|
||||
logger.info('[useFeaturedContent] catalogs:items', { total: allItems.length, duration: `${Date.now() - tFlat}ms` });
|
||||
|
||||
// Sort by popular, newest, etc. (possibly enhanced later)
|
||||
formattedContent = allItems.sort(() => Math.random() - 0.5).slice(0, 10);
|
||||
// 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 || 'metahub';
|
||||
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
|
||||
|
||||
const enrichLogo = async (item: any): Promise<StreamingContent> => {
|
||||
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,
|
||||
description: (item as any).description,
|
||||
year: (item as any).year,
|
||||
genres: (item as any).genres,
|
||||
inLibrary: Boolean((item as any).inLibrary),
|
||||
};
|
||||
try {
|
||||
const rawId = String(item.id);
|
||||
const isTmdb = rawId.startsWith('tmdb:');
|
||||
const isImdb = rawId.startsWith('tt');
|
||||
let tmdbId: string | null = null;
|
||||
let imdbId: string | null = null;
|
||||
|
||||
if (isTmdb) tmdbId = rawId.split(':')[1];
|
||||
if (isImdb) imdbId = rawId.split(':')[0];
|
||||
if (!tmdbId && imdbId) {
|
||||
const found = await tmdbService.findTMDBIdByIMDB(imdbId);
|
||||
tmdbId = found ? String(found) : null;
|
||||
}
|
||||
if (!tmdbId && !imdbId) return base;
|
||||
if (preference === 'tmdb') {
|
||||
logger.debug('[useFeaturedContent] logo:try:tmdb', { name: item.name, id: item.id, tmdbId, lang: preferredLanguage });
|
||||
if (!tmdbId) return base;
|
||||
const logoUrl = await tmdbService.getContentLogo(item.type === 'series' ? 'tv' : 'movie', tmdbId as string, preferredLanguage);
|
||||
if (logoUrl) {
|
||||
logger.debug('[useFeaturedContent] logo:tmdb:ok', { name: item.name, id: item.id, url: logoUrl, lang: preferredLanguage });
|
||||
return { ...base, logo: logoUrl };
|
||||
}
|
||||
// fallback metahub
|
||||
if (!imdbId && tmdbId) {
|
||||
const details: any = item.type === 'series' ? await tmdbService.getShowExternalIds(parseInt(tmdbId)) : await tmdbService.getMovieDetails(tmdbId);
|
||||
imdbId = details?.imdb_id;
|
||||
}
|
||||
if (imdbId) {
|
||||
const url = `https://images.metahub.space/logo/medium/${imdbId}/img`;
|
||||
logger.debug('[useFeaturedContent] logo:fallback:metahub', { name: item.name, id: item.id, url });
|
||||
return { ...base, logo: url };
|
||||
}
|
||||
return base;
|
||||
} else {
|
||||
// metahub first
|
||||
if (!imdbId && tmdbId) {
|
||||
const details: any = item.type === 'series' ? await tmdbService.getShowExternalIds(parseInt(tmdbId)) : await tmdbService.getMovieDetails(tmdbId);
|
||||
imdbId = details?.imdb_id;
|
||||
}
|
||||
if (imdbId) {
|
||||
const url = `https://images.metahub.space/logo/medium/${imdbId}/img`;
|
||||
logger.debug('[useFeaturedContent] logo:metahub:ok', { name: item.name, id: item.id, url });
|
||||
return { ...base, logo: url };
|
||||
}
|
||||
logger.debug('[useFeaturedContent] logo:metahub:miss → fallback:tmdb', { name: item.name, id: item.id, lang: preferredLanguage });
|
||||
if (!tmdbId) return base;
|
||||
const logoUrl = await tmdbService.getContentLogo(item.type === 'series' ? 'tv' : 'movie', tmdbId as string, preferredLanguage);
|
||||
if (logoUrl) {
|
||||
logger.debug('[useFeaturedContent] logo:tmdb:fallback:ok', { name: item.name, id: item.id, url: logoUrl, lang: preferredLanguage });
|
||||
return { ...base, logo: logoUrl };
|
||||
}
|
||||
return base;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[useFeaturedContent] logo:error', { name: item.name, id: item.id, error: String(error) });
|
||||
return base;
|
||||
}
|
||||
};
|
||||
|
||||
formattedContent = await Promise.all(topItems.map(enrichLogo));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -293,13 +431,17 @@ export function useFeaturedContent() {
|
|||
const settingsChanged =
|
||||
persistentStore.lastSettings.showHeroSection !== settings.showHeroSection ||
|
||||
persistentStore.lastSettings.featuredContentSource !== settings.featuredContentSource ||
|
||||
JSON.stringify(persistentStore.lastSettings.selectedHeroCatalogs) !== JSON.stringify(settings.selectedHeroCatalogs);
|
||||
JSON.stringify(persistentStore.lastSettings.selectedHeroCatalogs) !== JSON.stringify(settings.selectedHeroCatalogs) ||
|
||||
persistentStore.lastSettings.logoSourcePreference !== settings.logoSourcePreference ||
|
||||
persistentStore.lastSettings.tmdbLanguagePreference !== settings.tmdbLanguagePreference;
|
||||
|
||||
// Update our tracking of last used settings
|
||||
persistentStore.lastSettings = {
|
||||
showHeroSection: settings.showHeroSection,
|
||||
featuredContentSource: settings.featuredContentSource,
|
||||
selectedHeroCatalogs: [...settings.selectedHeroCatalogs]
|
||||
selectedHeroCatalogs: [...settings.selectedHeroCatalogs],
|
||||
logoSourcePreference: settings.logoSourcePreference,
|
||||
tmdbLanguagePreference: settings.tmdbLanguagePreference
|
||||
};
|
||||
|
||||
// Force refresh if settings changed during app restart
|
||||
|
|
@ -315,20 +457,29 @@ export function useFeaturedContent() {
|
|||
// Always reflect settings immediately in this hook
|
||||
const nextSource = settings.featuredContentSource;
|
||||
const nextSelected = settings.selectedHeroCatalogs || [];
|
||||
const nextLogoPref = settings.logoSourcePreference;
|
||||
const nextTmdbLang = settings.tmdbLanguagePreference;
|
||||
|
||||
const sourceChanged = contentSource !== nextSource;
|
||||
const catalogsChanged = JSON.stringify(selectedCatalogs) !== JSON.stringify(nextSelected);
|
||||
const logoPrefChanged = persistentStore.lastSettings.logoSourcePreference !== nextLogoPref;
|
||||
const tmdbLangChanged = persistentStore.lastSettings.tmdbLanguagePreference !== nextTmdbLang;
|
||||
|
||||
if (sourceChanged || (nextSource === 'catalogs' && catalogsChanged)) {
|
||||
if (sourceChanged || (nextSource === 'catalogs' && catalogsChanged) || logoPrefChanged || tmdbLangChanged) {
|
||||
logger.info('[useFeaturedContent] event:settings-changed:immediate-refresh', {
|
||||
fromSource: contentSource,
|
||||
toSource: nextSource,
|
||||
catalogsChanged
|
||||
catalogsChanged,
|
||||
logoPrefChanged,
|
||||
tmdbLangChanged
|
||||
});
|
||||
|
||||
// Update internal state immediately so dependent effects are in sync
|
||||
setContentSource(nextSource);
|
||||
setSelectedCatalogs(nextSelected);
|
||||
// Update tracked last settings for subsequent comparisons
|
||||
persistentStore.lastSettings.logoSourcePreference = nextLogoPref;
|
||||
persistentStore.lastSettings.tmdbLanguagePreference = nextTmdbLang;
|
||||
|
||||
// Clear current data to reflect change instantly in UI
|
||||
setAllFeaturedContent([]);
|
||||
|
|
|
|||
Loading…
Reference in a new issue