some UI optimization

This commit is contained in:
tapframe 2025-09-28 11:44:32 +05:30
parent 57036aaffb
commit b43957e6f9
5 changed files with 86 additions and 96 deletions

48
App.tsx
View file

@ -144,37 +144,29 @@ const ThemedApp = () => {
const shouldShowApp = isAppReady && hasCompletedOnboarding !== null; const shouldShowApp = isAppReady && hasCompletedOnboarding !== null;
const initialRouteName = hasCompletedOnboarding ? 'MainTabs' : 'Onboarding'; const initialRouteName = hasCompletedOnboarding ? 'MainTabs' : 'Onboarding';
const NavigationWithRef = () => {
const { navigationRef } = useAccount() as any;
return (
<NavigationContainer
ref={navigationRef as any}
theme={customNavigationTheme}
linking={undefined}
>
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar style="light" />
{!isAppReady && <SplashScreen onFinish={handleSplashComplete} />}
{shouldShowApp && <AppNavigator initialRouteName={initialRouteName} />}
{Platform.OS === 'ios' && (
<UpdatePopup
visible={showUpdatePopup}
updateInfo={updateInfo}
onUpdateNow={handleUpdateNow}
onUpdateLater={handleUpdateLater}
onDismiss={handleDismiss}
isInstalling={isInstalling}
/>
)}
</View>
</NavigationContainer>
);
};
return ( return (
<AccountProvider> <AccountProvider>
<PaperProvider theme={customDarkTheme}> <PaperProvider theme={customDarkTheme}>
<NavigationWithRef /> <NavigationContainer
theme={customNavigationTheme}
linking={undefined}
>
<View style={[styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}>
<StatusBar style="light" />
{!isAppReady && <SplashScreen onFinish={handleSplashComplete} />}
{shouldShowApp && <AppNavigator initialRouteName={initialRouteName} />}
{Platform.OS === 'ios' && (
<UpdatePopup
visible={showUpdatePopup}
updateInfo={updateInfo}
onUpdateNow={handleUpdateNow}
onUpdateLater={handleUpdateLater}
onDismiss={handleDismiss}
isInstalling={isInstalling}
/>
)}
</View>
</NavigationContainer>
</PaperProvider> </PaperProvider>
</AccountProvider> </AccountProvider>
); );

View file

@ -56,49 +56,32 @@ const POSTER_WIDTH = posterLayout.posterWidth;
const CatalogSection = ({ catalog }: CatalogSectionProps) => { const CatalogSection = ({ catalog }: CatalogSectionProps) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
// Simplified visibility tracking to reduce state updates and re-renders // Simplified visibility tracking - just load all images immediately for better performance
const [visibleIndexSet, setVisibleIndexSet] = useState<Set<number>>(new Set([0, 1, 2, 3, 4, 5, 6, 7])); const [hasLoaded, setHasLoaded] = useState(false);
const viewabilityConfig = useMemo(() => ({
itemVisiblePercentThreshold: 15,
minimumViewTime: 100,
}), []);
const onViewableItemsChanged = useRef(({ viewableItems }: { viewableItems: Array<{ index?: number | null }> }) => { // Load all images after a short delay to prevent blocking initial render
const next = new Set<number>(); React.useEffect(() => {
viewableItems.forEach(v => { if (typeof v.index === 'number') next.add(v.index); }); const timer = setTimeout(() => {
// Only pre-warm immediate neighbors to reduce overhead setHasLoaded(true);
const neighbors: number[] = []; }, 100);
next.forEach(i => { return () => clearTimeout(timer);
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]);
const handleContentPress = useCallback((id: string, type: string) => { const handleContentPress = useCallback((id: string, type: string) => {
navigation.navigate('Metadata', { id, type, addonId: catalog.addon }); navigation.navigate('Metadata', { id, type, addonId: catalog.addon });
}, [navigation, catalog.addon]); }, [navigation, catalog.addon]);
const renderContentItem = useCallback(({ item, index }: { item: StreamingContent, index: number }) => { const renderContentItem = useCallback(({ item, index }: { item: StreamingContent, index: number }) => {
// Simplify visibility logic to reduce re-renders // Load images immediately for better scrolling performance
const isVisible = visibleIndexSet.has(index) || index < 8;
return ( return (
<ContentItem <ContentItem
item={item} item={item}
onPress={handleContentPress} onPress={handleContentPress}
shouldLoadImage={isVisible} shouldLoadImage={hasLoaded}
deferMs={0} deferMs={index * 10} // Small stagger to prevent blocking
/> />
); );
}, [handleContentPress, visibleIndexSet]); }, [handleContentPress, hasLoaded]);
// Memoize the ItemSeparatorComponent to prevent re-creation // Memoize the ItemSeparatorComponent to prevent re-creation
const ItemSeparator = useCallback(() => <View style={{ width: 8 }} />, []); const ItemSeparator = useCallback(() => <View style={{ width: 8 }} />, []);
@ -139,8 +122,6 @@ const CatalogSection = ({ catalog }: CatalogSectionProps) => {
onEndReachedThreshold={0.7} onEndReachedThreshold={0.7}
onEndReached={() => {}} onEndReached={() => {}}
scrollEventThrottle={64} scrollEventThrottle={64}
viewabilityConfig={viewabilityConfig as any}
onViewableItemsChanged={onViewableItemsChanged.current as any}
removeClippedSubviews={true} removeClippedSubviews={true}
/> />
</Animated.View> </Animated.View>

View file

@ -74,7 +74,6 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
const [isWatched, setIsWatched] = useState(false); const [isWatched, setIsWatched] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false); const [imageLoaded, setImageLoaded] = useState(false);
const [imageError, setImageError] = useState(false); const [imageError, setImageError] = useState(false);
const [shouldLoadImageState, setShouldLoadImageState] = useState(false);
const [retryCount, setRetryCount] = useState(0); const [retryCount, setRetryCount] = useState(0);
const { currentTheme } = useTheme(); const { currentTheme } = useTheme();
const { settings, isLoaded } = useSettings(); const { settings, isLoaded } = useSettings();
@ -126,22 +125,6 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
setMenuVisible(false); 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 // Memoize optimized poster URL to prevent recalculating
const optimizedPosterUrl = React.useMemo(() => { const optimizedPosterUrl = React.useMemo(() => {
@ -213,16 +196,16 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
delayLongPress={300} delayLongPress={300}
> >
<View ref={itemRef} style={[styles.contentItemContainer, { borderRadius: posterRadius }] }> <View ref={itemRef} style={[styles.contentItemContainer, { borderRadius: posterRadius }] }>
{/* Only load image when shouldLoadImage is true (lazy loading) */} {/* Always load image for horizontal scrolling to prevent blank posters */}
{(shouldLoadImageProp ?? shouldLoadImageState) && item.poster ? ( {item.poster ? (
<ExpoImage <ExpoImage
source={{ uri: optimizedPosterUrl }} source={{ uri: optimizedPosterUrl }}
style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, borderRadius: posterRadius }]} style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, borderRadius: posterRadius }]}
contentFit="cover" contentFit="cover"
cachePolicy={Platform.OS === 'android' ? 'disk' : 'memory-disk'} cachePolicy={Platform.OS === 'android' ? 'disk' : 'memory-disk'}
transition={140} transition={100} // Faster transition for scrolling
allowDownscaling allowDownscaling
priority="low" // Deprioritize decode for long lists priority="normal" // Normal priority for horizontal scrolling
onLoad={() => { onLoad={() => {
setImageLoaded(true); setImageLoaded(true);
setImageError(false); setImageError(false);
@ -241,7 +224,7 @@ const ContentItem = ({ item, onPress, shouldLoadImage: shouldLoadImageProp, defe
recyclingKey={item.id} // Add recycling key for better performance recyclingKey={item.id} // Add recycling key for better performance
/> />
) : ( ) : (
// Show placeholder until lazy load triggers // Show placeholder for items without posters
<View style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, justifyContent: 'center', alignItems: 'center', borderRadius: posterRadius }] }> <View style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, justifyContent: 'center', alignItems: 'center', borderRadius: posterRadius }] }>
<Text style={{ color: currentTheme.colors.textMuted, fontSize: 10, textAlign: 'center' }}> <Text style={{ color: currentTheme.colors.textMuted, fontSize: 10, textAlign: 'center' }}>
{item.name.substring(0, 20)}... {item.name.substring(0, 20)}...
@ -349,9 +332,8 @@ const styles = StyleSheet.create({
}); });
export default React.memo(ContentItem, (prev, next) => { 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.id !== next.item.id) return false;
if (prev.item.poster !== next.item.poster) return false; if (prev.item.poster !== next.item.poster) return false;
if ((prev.shouldLoadImage ?? false) !== (next.shouldLoadImage ?? false)) return false;
return true; return true;
}); });

View file

@ -48,21 +48,35 @@ export const AccountProvider: React.FC<{ children: React.ReactNode }> = ({ child
}); });
// Auth state listener // Auth state listener
const { data: subscription } = supabase.auth.onAuthStateChange(async (_event, session) => { const { data: subscription } = supabase.auth.onAuthStateChange(async (event, session) => {
setLoading(true); // Only set loading for actual auth changes, not initial session
if (event !== 'INITIAL_SESSION') {
setLoading(true);
}
try { try {
const fullUser = session?.user ? await accountService.getCurrentUser() : null; const fullUser = session?.user ? await accountService.getCurrentUser() : null;
setUser(fullUser); setUser(fullUser);
// Immediately clear loading so UI can transition to MainTabs/Auth
setLoading(false);
if (fullUser) { if (fullUser) {
await syncService.migrateLocalScopeToUser(); // Run sync in background without blocking UI
await syncService.subscribeRealtime(); setTimeout(async () => {
// Pull first to hydrate local state, then push to avoid wiping server with empty local try {
await syncService.fullPull(); await syncService.migrateLocalScopeToUser();
await syncService.fullPush(); 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 { } else {
syncService.unsubscribeRealtime(); syncService.unsubscribeRealtime();
} }
} finally { } catch (e) {
setLoading(false); setLoading(false);
} }
}); });

View file

@ -588,6 +588,20 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
return (availableWidth - totalSpacing) / effectiveNumColumns; return (availableWidth - totalSpacing) / effectiveNumColumns;
}, [effectiveNumColumns, screenData.width, screenData.itemWidth]); }, [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 }) => { const renderItem = useCallback(({ item, index }: { item: Meta; index: number }) => {
// Calculate if this is the last item in a row // Calculate if this is the last item in a row
const isLastInRow = (index + 1) % effectiveNumColumns === 0; const isLastInRow = (index + 1) % effectiveNumColumns === 0;
@ -607,12 +621,14 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
activeOpacity={0.7} activeOpacity={0.7}
> >
<Image <Image
source={{ uri: item.poster || 'https://via.placeholder.com/300x450/333333/666666?text=No+Image' }} source={{ uri: optimizePosterUrl(item.poster) }}
style={styles.poster} style={styles.poster}
contentFit="cover" contentFit="cover"
cachePolicy="disk" cachePolicy={Platform.OS === 'android' ? 'memory-disk' : 'memory-disk'}
transition={0} transition={100}
allowDownscaling allowDownscaling
priority="normal"
recyclingKey={`${item.id}-${item.type}`}
/> />
{type === 'movie' && nowPlayingMovies.has(item.id) && ( {type === 'movie' && nowPlayingMovies.has(item.id) && (
@ -644,7 +660,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
)} )}
</TouchableOpacity> </TouchableOpacity>
); );
}, [navigation, styles, effectiveNumColumns, effectiveItemWidth, type, nowPlayingMovies]); }, [navigation, styles, effectiveNumColumns, effectiveItemWidth, type, nowPlayingMovies, colors.white, optimizePosterUrl]);
const renderEmptyState = () => ( const renderEmptyState = () => (
<View style={styles.centered}> <View style={styles.centered}>
@ -754,6 +770,11 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
} }
contentContainerStyle={styles.list} contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
removeClippedSubviews={true}
maxToRenderPerBatch={effectiveNumColumns * 3}
windowSize={10}
initialNumToRender={effectiveNumColumns * 4}
getItemType={() => 'item'}
/> />
) : renderEmptyState()} ) : renderEmptyState()}
</SafeAreaView> </SafeAreaView>