diff --git a/App.tsx b/App.tsx index 4fe3dc6..da0ca8d 100644 --- a/App.tsx +++ b/App.tsx @@ -144,37 +144,29 @@ const ThemedApp = () => { const shouldShowApp = isAppReady && hasCompletedOnboarding !== null; const initialRouteName = hasCompletedOnboarding ? 'MainTabs' : 'Onboarding'; - const NavigationWithRef = () => { - const { navigationRef } = useAccount() as any; - return ( - - - - {!isAppReady && } - {shouldShowApp && } - {Platform.OS === 'ios' && ( - - )} - - - ); - }; - return ( - + + + + {!isAppReady && } + {shouldShowApp && } + {Platform.OS === 'ios' && ( + + )} + + ); diff --git a/src/components/home/CatalogSection.tsx b/src/components/home/CatalogSection.tsx index 88560cf..f09d544 100644 --- a/src/components/home/CatalogSection.tsx +++ b/src/components/home/CatalogSection.tsx @@ -56,49 +56,32 @@ const POSTER_WIDTH = posterLayout.posterWidth; const CatalogSection = ({ catalog }: CatalogSectionProps) => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); - // Simplified visibility tracking to reduce state updates and re-renders - const [visibleIndexSet, setVisibleIndexSet] = useState>(new Set([0, 1, 2, 3, 4, 5, 6, 7])); - const viewabilityConfig = useMemo(() => ({ - itemVisiblePercentThreshold: 15, - minimumViewTime: 100, - }), []); + // Simplified visibility tracking - just load all images immediately for better performance + const [hasLoaded, setHasLoaded] = useState(false); - const onViewableItemsChanged = useRef(({ viewableItems }: { viewableItems: Array<{ index?: number | null }> }) => { - const next = new Set(); - viewableItems.forEach(v => { if (typeof v.index === 'number') next.add(v.index); }); - // Only pre-warm immediate neighbors to reduce overhead - const neighbors: number[] = []; - next.forEach(i => { - neighbors.push(i - 1, i + 1); - }); - neighbors.forEach(i => { if (i >= 0) next.add(i); }); - setVisibleIndexSet(next); - }); - - const [minVisible, maxVisible] = useMemo(() => { - if (visibleIndexSet.size === 0) return [0, 7]; - let min = Number.POSITIVE_INFINITY; - let max = 0; - visibleIndexSet.forEach(i => { if (i < min) min = i; if (i > max) max = i; }); - return [min, max]; - }, [visibleIndexSet]); + // Load all images after a short delay to prevent blocking initial render + React.useEffect(() => { + const timer = setTimeout(() => { + setHasLoaded(true); + }, 100); + return () => clearTimeout(timer); + }, []); const handleContentPress = useCallback((id: string, type: string) => { navigation.navigate('Metadata', { id, type, addonId: catalog.addon }); }, [navigation, catalog.addon]); const renderContentItem = useCallback(({ item, index }: { item: StreamingContent, index: number }) => { - // Simplify visibility logic to reduce re-renders - const isVisible = visibleIndexSet.has(index) || index < 8; + // Load images immediately for better scrolling performance return ( ); - }, [handleContentPress, visibleIndexSet]); + }, [handleContentPress, hasLoaded]); // Memoize the ItemSeparatorComponent to prevent re-creation const ItemSeparator = useCallback(() => , []); @@ -139,8 +122,6 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => { onEndReachedThreshold={0.7} onEndReached={() => {}} scrollEventThrottle={64} - viewabilityConfig={viewabilityConfig as any} - onViewableItemsChanged={onViewableItemsChanged.current as any} removeClippedSubviews={true} /> diff --git a/src/components/home/ContentItem.tsx b/src/components/home/ContentItem.tsx index 4ad0774..dc2e839 100644 --- a/src/components/home/ContentItem.tsx +++ b/src/components/home/ContentItem.tsx @@ -74,7 +74,6 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe const [isWatched, setIsWatched] = useState(false); const [imageLoaded, setImageLoaded] = useState(false); const [imageError, setImageError] = useState(false); - const [shouldLoadImageState, setShouldLoadImageState] = useState(false); const [retryCount, setRetryCount] = useState(0); const { currentTheme } = useTheme(); const { settings, isLoaded } = useSettings(); @@ -126,22 +125,6 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe setMenuVisible(false); }, []); - // Lazy load images - only load when asked by parent (viewability) or after small defer - useEffect(() => { - if (shouldLoadImageProp !== undefined) { - if (shouldLoadImageProp) { - const t = setTimeout(() => setShouldLoadImageState(true), deferMs); - return () => clearTimeout(t); - } else { - setShouldLoadImageState(false); - } - return; - } - const timer = setTimeout(() => { - setShouldLoadImageState(true); - }, 80); - return () => clearTimeout(timer); - }, [shouldLoadImageProp, deferMs]); // Memoize optimized poster URL to prevent recalculating const optimizedPosterUrl = React.useMemo(() => { @@ -213,16 +196,16 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe delayLongPress={300} > - {/* Only load image when shouldLoadImage is true (lazy loading) */} - {(shouldLoadImageProp ?? shouldLoadImageState) && item.poster ? ( + {/* Always load image for horizontal scrolling to prevent blank posters */} + {item.poster ? ( { setImageLoaded(true); setImageError(false); @@ -241,7 +224,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe recyclingKey={item.id} // Add recycling key for better performance /> ) : ( - // Show placeholder until lazy load triggers + // Show placeholder for items without posters {item.name.substring(0, 20)}... @@ -349,9 +332,8 @@ const styles = StyleSheet.create({ }); export default React.memo(ContentItem, (prev, next) => { - // Re-render when identity changes or when visibility-driven loading flips + // Re-render when identity changes or poster changes if (prev.item.id !== next.item.id) return false; if (prev.item.poster !== next.item.poster) return false; - if ((prev.shouldLoadImage ?? false) !== (next.shouldLoadImage ?? false)) return false; return true; }); \ No newline at end of file diff --git a/src/contexts/AccountContext.tsx b/src/contexts/AccountContext.tsx index 0359d3e..f82fff4 100644 --- a/src/contexts/AccountContext.tsx +++ b/src/contexts/AccountContext.tsx @@ -48,21 +48,35 @@ export const AccountProvider: React.FC<{ children: React.ReactNode }> = ({ child }); // Auth state listener - const { data: subscription } = supabase.auth.onAuthStateChange(async (_event, session) => { - setLoading(true); + const { data: subscription } = supabase.auth.onAuthStateChange(async (event, session) => { + // Only set loading for actual auth changes, not initial session + if (event !== 'INITIAL_SESSION') { + setLoading(true); + } try { const fullUser = session?.user ? await accountService.getCurrentUser() : null; setUser(fullUser); + // Immediately clear loading so UI can transition to MainTabs/Auth + setLoading(false); if (fullUser) { - await syncService.migrateLocalScopeToUser(); - await syncService.subscribeRealtime(); - // Pull first to hydrate local state, then push to avoid wiping server with empty local - await syncService.fullPull(); - await syncService.fullPush(); + // Run sync in background without blocking UI + setTimeout(async () => { + try { + await syncService.migrateLocalScopeToUser(); + await new Promise(r => setTimeout(r, 0)); + await syncService.subscribeRealtime(); + await new Promise(r => setTimeout(r, 0)); + await syncService.fullPull(); + await new Promise(r => setTimeout(r, 0)); + await syncService.fullPush(); + } catch (error) { + console.warn('[AccountContext] Background sync failed:', error); + } + }, 0); } else { syncService.unsubscribeRealtime(); } - } finally { + } catch (e) { setLoading(false); } }); diff --git a/src/screens/CatalogScreen.tsx b/src/screens/CatalogScreen.tsx index b6f4771..95d7695 100644 --- a/src/screens/CatalogScreen.tsx +++ b/src/screens/CatalogScreen.tsx @@ -588,6 +588,20 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { return (availableWidth - totalSpacing) / effectiveNumColumns; }, [effectiveNumColumns, screenData.width, screenData.itemWidth]); + // Helper function to optimize poster URLs + const optimizePosterUrl = useCallback((poster: string | undefined) => { + if (!poster || poster.includes('placeholder')) { + return 'https://via.placeholder.com/300x450/333333/666666?text=No+Image'; + } + + // For TMDB images, use smaller sizes for better performance + if (poster.includes('image.tmdb.org')) { + return poster.replace(/\/w\d+\//, '/w300/'); + } + + return poster; + }, []); + const renderItem = useCallback(({ item, index }: { item: Meta; index: number }) => { // Calculate if this is the last item in a row const isLastInRow = (index + 1) % effectiveNumColumns === 0; @@ -607,12 +621,14 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { activeOpacity={0.7} > {type === 'movie' && nowPlayingMovies.has(item.id) && ( @@ -644,7 +660,7 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { )} ); - }, [navigation, styles, effectiveNumColumns, effectiveItemWidth, type, nowPlayingMovies]); + }, [navigation, styles, effectiveNumColumns, effectiveItemWidth, type, nowPlayingMovies, colors.white, optimizePosterUrl]); const renderEmptyState = () => ( @@ -754,6 +770,11 @@ const CatalogScreen: React.FC = ({ route, navigation }) => { } contentContainerStyle={styles.list} showsVerticalScrollIndicator={false} + removeClippedSubviews={true} + maxToRenderPerBatch={effectiveNumColumns * 3} + windowSize={10} + initialNumToRender={effectiveNumColumns * 4} + getItemType={() => 'item'} /> ) : renderEmptyState()}