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()}