fixed herosection loading issue

This commit is contained in:
tapframe 2025-08-29 19:34:28 +05:30
parent 9eea58ef98
commit 12c591216e
4 changed files with 126 additions and 130 deletions

View file

@ -49,6 +49,10 @@ const { width, height } = Dimensions.get('window');
// Utility to determine if device is tablet-sized
const isTablet = width >= 768;
// Simple perf timer helper
const nowMs = () => Date.now();
const since = (start: number) => `${(nowMs() - start).toFixed(0)}ms`;
const NoFeaturedContent = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
@ -142,6 +146,19 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
const [logoLoadError, setLogoLoadError] = useState(false);
// Add a ref to track logo fetch in progress
const logoFetchInProgress = useRef<boolean>(false);
const firstRenderTsRef = useRef<number>(nowMs());
const lastContentChangeTsRef = useRef<number>(0);
// Initial diagnostics
useEffect(() => {
logger.info('[FeaturedContent] mounted', {
isTablet,
screen: { width, height },
});
return () => {
logger.info('[FeaturedContent] unmounted');
};
}, []);
// Enhanced poster transition animations
const posterScale = useSharedValue(1);
@ -185,6 +202,8 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
// Preload the image
const preloadImage = async (url: string): Promise<boolean> => {
const t0 = nowMs();
logger.debug('[FeaturedContent] preloadImage:start', { url });
// Skip if already cached to prevent redundant prefetch
if (imageCache[url]) return true;
@ -205,10 +224,12 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
timeout,
]);
imageCache[url] = true;
logger.debug('[FeaturedContent] preloadImage:success', { url, duration: since(t0) });
return true;
} catch (error) {
// Clear any partial cache entry on error
delete imageCache[url];
logger.warn('[FeaturedContent] preloadImage:error', { url, duration: since(t0), error: String(error) });
return false;
}
};
@ -224,6 +245,8 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
const fetchLogo = async () => {
logoFetchInProgress.current = true;
const t0 = nowMs();
logger.info('[FeaturedContent] fetchLogo:start', { id: featuredContent?.id, type: featuredContent?.type });
try {
const contentId = featuredContent.id;
@ -234,8 +257,10 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
const logoPreference = settings.logoSourcePreference || 'metahub';
const preferredLanguage = settings.tmdbLanguagePreference || 'en';
// Reset state for new fetch
setLogoUrl(null);
// Reset state for new fetch only if switching to a different item
if (prevContentIdRef.current !== contentId) {
setLogoUrl(null);
}
setLogoLoadError(false);
// Extract IDs
@ -274,6 +299,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
let fallbackAttempted = false;
// --- Logo Fetching Logic ---
logger.debug('[FeaturedContent] fetchLogo:ids', { imdbId, tmdbId, preference: logoPreference, lang: preferredLanguage });
if (logoPreference === 'metahub') {
// Primary: Metahub (needs imdbId)
@ -281,9 +307,11 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
primaryAttempted = true;
const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`;
try {
const tHead = nowMs();
const response = await fetch(metahubUrl, { method: 'HEAD' });
if (response.ok) {
finalLogoUrl = metahubUrl;
logger.debug('[FeaturedContent] fetchLogo:metahub:ok', { url: metahubUrl, duration: since(tHead) });
}
} catch (error) { /* Log if needed */ }
}
@ -293,9 +321,11 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
fallbackAttempted = true;
try {
const tmdbService = TMDBService.getInstance();
const tTmdb = nowMs();
const logoUrl = await tmdbService.getContentLogo(tmdbType, tmdbId, preferredLanguage);
if (logoUrl) {
finalLogoUrl = logoUrl;
logger.debug('[FeaturedContent] fetchLogo:tmdb:fallback:ok', { url: logoUrl, duration: since(tTmdb) });
}
} catch (error) { /* Log if needed */ }
}
@ -306,9 +336,11 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
primaryAttempted = true;
try {
const tmdbService = TMDBService.getInstance();
const tTmdb = nowMs();
const logoUrl = await tmdbService.getContentLogo(tmdbType, tmdbId, preferredLanguage);
if (logoUrl) {
finalLogoUrl = logoUrl;
logger.debug('[FeaturedContent] fetchLogo:tmdb:ok', { url: logoUrl, duration: since(tTmdb) });
}
} catch (error) { /* Log if needed */ }
}
@ -318,9 +350,11 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
fallbackAttempted = true;
const metahubUrl = `https://images.metahub.space/logo/medium/${imdbId}/img`;
try {
const tHead = nowMs();
const response = await fetch(metahubUrl, { method: 'HEAD' });
if (response.ok) {
finalLogoUrl = metahubUrl;
logger.debug('[FeaturedContent] fetchLogo:metahub:fallback:ok', { url: metahubUrl, duration: since(tHead) });
}
} catch (error) { /* Log if needed */ }
}
@ -329,18 +363,22 @@ 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) });
} 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) });
} else {
// No logo found from any source
setLogoLoadError(true);
logger.warn('[FeaturedContent] fetchLogo:none', { id: contentId, primaryAttempted, fallbackAttempted, duration: since(t0) });
// logger.warn(`[FeaturedContent] No logo found for ${contentData.name} (${contentId}) with preference ${logoPreference}. Primary attempted: ${primaryAttempted}, Fallback attempted: ${fallbackAttempted}`);
}
} catch (error) {
// logger.error('[FeaturedContent] Error in fetchLogo:', error);
setLogoLoadError(true);
logger.error('[FeaturedContent] fetchLogo:error', { error: String(error), duration: since(t0) });
} finally {
logoFetchInProgress.current = false;
}
@ -357,6 +395,8 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
const posterUrl = featuredContent.banner || featuredContent.poster;
const contentId = featuredContent.id;
const isContentChange = contentId !== prevContentIdRef.current;
const t0 = nowMs();
logger.info('[FeaturedContent] content:update', { id: contentId, isContentChange, posterUrlExists: Boolean(posterUrl), sinceMount: since(firstRenderTsRef.current) });
// Enhanced content change detection and animations
if (isContentChange) {
@ -405,6 +445,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
// Load poster with enhanced transition
if (posterUrl) {
const tPoster = nowMs();
const posterSuccess = await preloadImage(posterUrl);
if (posterSuccess) {
// Animate in new poster with scale and fade
@ -420,6 +461,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
duration: 600,
easing: Easing.out(Easing.cubic)
});
logger.debug('[FeaturedContent] poster:ready', { id: contentId, duration: since(tPoster) });
// Animate content back in with delay
contentOpacity.value = withDelay(200, withTiming(1, {
@ -435,16 +477,20 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
// Load logo if available with enhanced timing
if (logoUrl) {
const tLogo = nowMs();
const logoSuccess = await preloadImage(logoUrl);
if (logoSuccess) {
logoOpacity.value = withDelay(500, withTiming(1, {
duration: 600,
easing: Easing.out(Easing.cubic)
}));
logger.debug('[FeaturedContent] logo:ready', { id: contentId, duration: since(tLogo) });
} else {
setLogoLoadError(true);
logger.warn('[FeaturedContent] logo:failed', { id: contentId, duration: since(tLogo) });
}
}
logger.info('[FeaturedContent] images:load:done', { id: contentId, total: since(t0) });
};
loadImages();
@ -453,6 +499,7 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
const onLogoLoadError = () => {
setLogoLoaded(true); // Treat error as "loaded" to stop spinner
setLogoError(true);
logger.warn('[FeaturedContent] logo:onError', { id: featuredContent?.id, url: logoUrl });
};
const handleInfoPress = () => {
@ -464,13 +511,15 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin
}
};
// Show skeleton while loading to avoid empty state flash and sluggish feel
if (loading) {
// Show skeleton only if we're loading AND no content is available yet
if (loading && !featuredContent) {
logger.debug('[FeaturedContent] render:loading', { sinceMount: since(firstRenderTsRef.current) });
return <SkeletonFeatured />;
}
if (!featuredContent) {
// Suppress empty state while loading to avoid flash on startup/hydration
logger.debug('[FeaturedContent] render:no-featured-content', { sinceMount: since(firstRenderTsRef.current) });
return <NoFeaturedContent />;
}

View file

@ -31,8 +31,6 @@ interface MetadataSourceSelectorProps {
contentType: string;
onSourceChange: (sourceId: string, sourceType: 'addon' | 'tmdb') => void;
disabled?: boolean;
enableComplementary?: boolean;
onComplementaryToggle?: (enabled: boolean) => void;
}
const MetadataSourceSelector: React.FC<MetadataSourceSelectorProps> = ({
@ -41,8 +39,6 @@ const MetadataSourceSelector: React.FC<MetadataSourceSelectorProps> = ({
contentType,
onSourceChange,
disabled = false,
enableComplementary = false,
onComplementaryToggle,
}) => {
const { currentTheme } = useTheme();
const [isVisible, setIsVisible] = useState(false);
@ -292,46 +288,7 @@ const MetadataSourceSelector: React.FC<MetadataSourceSelectorProps> = ({
</TouchableOpacity>
</View>
{/* Complementary Metadata Toggle */}
<View style={styles.complementaryToggle}>
<View style={styles.toggleContent}>
<MaterialIcons
name="merge-type"
size={20}
color={currentTheme.colors.primary}
/>
<View style={styles.toggleText}>
<Text style={[styles.toggleTitle, { color: currentTheme.colors.text }]}>
Complementary Metadata
</Text>
<Text style={[styles.toggleDescription, { color: currentTheme.colors.textMuted }]}>
Fetch missing data from other sources
</Text>
</View>
</View>
<TouchableOpacity
style={[
styles.toggleSwitch,
{
backgroundColor: enableComplementary
? currentTheme.colors.primary
: currentTheme.colors.elevation2,
}
]}
onPress={() => onComplementaryToggle?.(!enableComplementary)}
activeOpacity={0.7}
>
<View
style={[
styles.toggleThumb,
{
transform: [{ translateX: enableComplementary ? 20 : 2 }],
backgroundColor: currentTheme.colors.white,
}
]}
/>
</TouchableOpacity>
</View>
{loading ? (
<View style={styles.loadingContainer}>
@ -561,51 +518,6 @@ const styles = StyleSheet.create({
checkContainer: {
padding: 4,
},
complementaryToggle: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255, 255, 255, 0.08)',
},
toggleContent: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
},
toggleText: {
marginLeft: 16,
flex: 1,
},
toggleTitle: {
fontSize: 16,
fontWeight: '600',
marginBottom: 2,
},
toggleDescription: {
fontSize: 13,
lineHeight: 18,
opacity: 0.8,
},
toggleSwitch: {
width: 44,
height: 24,
borderRadius: 12,
justifyContent: 'center',
paddingHorizontal: 2,
},
toggleThumb: {
width: 18,
height: 18,
borderRadius: 9,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: 2,
elevation: 2,
},
});
export default MetadataSourceSelector;

View file

@ -24,6 +24,7 @@ const persistentStore = {
// Cache timeout in milliseconds (e.g., 5 minutes)
const CACHE_TIMEOUT = 5 * 60 * 1000;
const STORAGE_KEY = 'featured_content_cache_v1';
const DISABLE_CACHE = true;
export function useFeaturedContent() {
const [featuredContent, setFeaturedContent] = useState<StreamingContent | null>(persistentStore.featuredContent);
@ -52,32 +53,42 @@ export function useFeaturedContent() {
}, []);
const loadFeaturedContent = useCallback(async (forceRefresh = false) => {
const t0 = Date.now();
logger.info('[useFeaturedContent] load:start', { forceRefresh, contentSource, selectedCatalogsCount: (selectedCatalogs || []).length });
// First, ensure contentSource matches current settings (could be outdated due to async updates)
if (contentSource !== settings.featuredContentSource) {
console.log(`Updating content source from ${contentSource} to ${settings.featuredContentSource}`);
logger.debug('[useFeaturedContent] load:source-mismatch', { from: contentSource, to: settings.featuredContentSource });
setContentSource(settings.featuredContentSource);
// We return here and let the effect triggered by contentSource change handle the loading
return;
}
// Check if we should use cached data
// Check if we should use cached data (disabled if DISABLE_CACHE)
const now = Date.now();
const cacheAge = now - persistentStore.lastFetchTime;
if (!forceRefresh &&
persistentStore.featuredContent &&
persistentStore.allFeaturedContent.length > 0 &&
cacheAge < CACHE_TIMEOUT) {
// Use cached data
console.log('Using cached featured content data');
setFeaturedContent(persistentStore.featuredContent);
setAllFeaturedContent(persistentStore.allFeaturedContent);
setLoading(false);
persistentStore.isFirstLoad = false;
return;
logger.debug('[useFeaturedContent] cache:status', {
disabled: DISABLE_CACHE,
hasFeatured: Boolean(persistentStore.featuredContent),
allCount: persistentStore.allFeaturedContent?.length || 0,
cacheAgeMs: cacheAge,
timeoutMs: CACHE_TIMEOUT,
});
if (!DISABLE_CACHE) {
if (!forceRefresh &&
persistentStore.featuredContent &&
persistentStore.allFeaturedContent.length > 0 &&
cacheAge < CACHE_TIMEOUT) {
// Use cached data
logger.info('[useFeaturedContent] cache:use', { duration: `${Date.now() - t0}ms` });
setFeaturedContent(persistentStore.featuredContent);
setAllFeaturedContent(persistentStore.allFeaturedContent);
setLoading(false);
persistentStore.isFirstLoad = false;
return;
}
}
console.log(`Loading featured content from ${contentSource}`);
logger.info('[useFeaturedContent] fetch:start', { source: contentSource });
setLoading(true);
cleanup();
abortControllerRef.current = new AbortController();
@ -88,7 +99,9 @@ export function useFeaturedContent() {
if (contentSource === 'tmdb') {
// Load from TMDB trending
const tTmdb = Date.now();
const trendingResults = await tmdbService.getTrending('movie', 'day');
logger.info('[useFeaturedContent] tmdb:trending', { count: trendingResults?.length || 0, duration: `${Date.now() - tTmdb}ms` });
if (signal.aborted) return;
@ -115,6 +128,7 @@ export function useFeaturedContent() {
});
// Then fetch logos for each item
const tLogos = Date.now();
formattedContent = await Promise.all(
preFormattedContent.map(async (item) => {
try {
@ -135,10 +149,13 @@ export function useFeaturedContent() {
}
})
);
logger.info('[useFeaturedContent] tmdb:logos', { count: formattedContent.length, duration: `${Date.now() - tLogos}ms` });
}
} else {
// Load from installed catalogs
const tCats = Date.now();
const catalogs = await catalogService.getHomeCatalogs();
logger.info('[useFeaturedContent] catalogs:list', { count: catalogs?.length || 0, duration: `${Date.now() - tCats}ms` });
if (signal.aborted) return;
@ -153,14 +170,17 @@ export function useFeaturedContent() {
return selectedCatalogs.includes(catalogId);
})
: catalogs; // Use all catalogs if none specifically selected
logger.debug('[useFeaturedContent] catalogs:filtered', { filteredCount: filteredCatalogs.length, selectedCount: selectedCatalogs?.length || 0 });
// Flatten all catalog items into a single array, filter out items without posters
const tFlat = Date.now();
const allItems = filteredCatalogs.flatMap(catalog => catalog.items)
.filter(item => item.poster)
.filter((item, index, self) =>
// Remove duplicates based on ID
index === self.findIndex(t => t.id === item.id)
);
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);
@ -171,6 +191,7 @@ export function useFeaturedContent() {
// Safety guard: if nothing came back within a reasonable time, stop loading
if (!formattedContent || formattedContent.length === 0) {
logger.warn('[useFeaturedContent] results:empty');
// Fall back to any cached featured item so UI can render something
const cachedJson = await AsyncStorage.getItem(STORAGE_KEY).catch(() => null);
if (cachedJson) {
@ -180,14 +201,17 @@ export function useFeaturedContent() {
formattedContent = Array.isArray(parsed.allFeaturedContent) && parsed.allFeaturedContent.length > 0
? parsed.allFeaturedContent
: [parsed.featuredContent];
logger.info('[useFeaturedContent] fallback:storage', { count: formattedContent.length });
}
} catch {}
}
}
// Update persistent store with the new data
// Update persistent store with the new data (no lastFetchTime when cache disabled)
persistentStore.allFeaturedContent = formattedContent;
persistentStore.lastFetchTime = now;
if (!DISABLE_CACHE) {
persistentStore.lastFetchTime = now;
}
persistentStore.isFirstLoad = false;
setAllFeaturedContent(formattedContent);
@ -196,40 +220,51 @@ export function useFeaturedContent() {
persistentStore.featuredContent = formattedContent[0];
setFeaturedContent(formattedContent[0]);
currentIndexRef.current = 0;
// Persist cache for fast startup
try {
await AsyncStorage.setItem(
STORAGE_KEY,
JSON.stringify({
ts: now,
featuredContent: formattedContent[0],
allFeaturedContent: formattedContent,
})
);
} catch {}
// Persist cache for fast startup (skipped when cache disabled)
if (!DISABLE_CACHE) {
try {
await AsyncStorage.setItem(
STORAGE_KEY,
JSON.stringify({
ts: now,
featuredContent: formattedContent[0],
allFeaturedContent: formattedContent,
})
);
logger.debug('[useFeaturedContent] cache:written', { firstId: formattedContent[0]?.id });
} catch {}
}
} else {
persistentStore.featuredContent = null;
setFeaturedContent(null);
// Clear persisted cache on empty
try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {}
// Clear persisted cache on empty (skipped when cache disabled)
if (!DISABLE_CACHE) {
try { await AsyncStorage.removeItem(STORAGE_KEY); } catch {}
}
}
} catch (error) {
if (signal.aborted) {
logger.info('Featured content fetch aborted');
logger.info('[useFeaturedContent] fetch:aborted');
} else {
logger.error('Failed to load featured content:', error);
logger.error('[useFeaturedContent] fetch:error', { error: String(error) });
}
setFeaturedContent(null);
setAllFeaturedContent([]);
} finally {
if (!signal.aborted) {
setLoading(false);
logger.info('[useFeaturedContent] load:done', { duration: `${Date.now() - t0}ms` });
}
}
}, [cleanup, genreMap, loadingGenres, contentSource, selectedCatalogs]);
// Hydrate from persisted cache immediately for instant render
useEffect(() => {
if (DISABLE_CACHE) {
// Skip hydration entirely
logger.debug('[useFeaturedContent] hydrate:skipped');
return;
}
let cancelled = false;
(async () => {
try {
@ -245,6 +280,7 @@ export function useFeaturedContent() {
setFeaturedContent(parsed.featuredContent);
setAllFeaturedContent(persistentStore.allFeaturedContent);
setLoading(false);
logger.info('[useFeaturedContent] hydrate:storage', { allCount: persistentStore.allFeaturedContent.length });
}
} catch {}
})();
@ -268,6 +304,7 @@ export function useFeaturedContent() {
// Force refresh if settings changed during app restart
if (settingsChanged) {
logger.info('[useFeaturedContent] settings:changed', { source: settings.featuredContentSource, selectedCount: settings.selectedHeroCatalogs?.length || 0 });
loadFeaturedContent(true);
}
}, [settings, loadFeaturedContent]);
@ -278,9 +315,7 @@ export function useFeaturedContent() {
// Only refresh if current content source is different from settings
// This prevents duplicate refreshes when HomeScreen also handles this event
if (contentSource !== settings.featuredContentSource) {
console.log('Content source changed, refreshing featured content');
console.log('Current content source:', contentSource);
console.log('New settings source:', settings.featuredContentSource);
logger.info('[useFeaturedContent] event:content-source-changed', { from: contentSource, to: settings.featuredContentSource });
// Content source will be updated in the next render cycle due to state updates
// No need to call loadFeaturedContent here as it will be triggered by contentSource change
} else if (
@ -288,7 +323,7 @@ export function useFeaturedContent() {
JSON.stringify(selectedCatalogs) !== JSON.stringify(settings.selectedHeroCatalogs)
) {
// Only refresh if using catalogs and selected catalogs changed
console.log('Selected catalogs changed, refreshing featured content');
logger.info('[useFeaturedContent] event:selected-catalogs-changed');
loadFeaturedContent(true);
}
};

View file

@ -2,7 +2,7 @@ import axios from 'axios';
import AsyncStorage from '@react-native-async-storage/async-storage';
// TMDB API configuration
const DEFAULT_API_KEY = '439c478a771f35c05022f9feabcca01c';
const DEFAULT_API_KEY = 'd131017ccc6e5462a81c9304d21476de';
const BASE_URL = 'https://api.themoviedb.org/3';
const TMDB_API_KEY_STORAGE_KEY = 'tmdb_api_key';
const USE_CUSTOM_TMDB_API_KEY = 'use_custom_tmdb_api_key';