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 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 (
<AccountProvider>
<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>
</AccountProvider>
);

View file

@ -56,49 +56,32 @@ const POSTER_WIDTH = posterLayout.posterWidth;
const CatalogSection = ({ catalog }: CatalogSectionProps) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { currentTheme } = useTheme();
// Simplified visibility tracking to reduce state updates and re-renders
const [visibleIndexSet, setVisibleIndexSet] = useState<Set<number>>(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<number>();
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 (
<ContentItem
item={item}
onPress={handleContentPress}
shouldLoadImage={isVisible}
deferMs={0}
shouldLoadImage={hasLoaded}
deferMs={index * 10} // Small stagger to prevent blocking
/>
);
}, [handleContentPress, visibleIndexSet]);
}, [handleContentPress, hasLoaded]);
// Memoize the ItemSeparatorComponent to prevent re-creation
const ItemSeparator = useCallback(() => <View style={{ width: 8 }} />, []);
@ -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}
/>
</Animated.View>

View file

@ -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}
>
<View ref={itemRef} style={[styles.contentItemContainer, { borderRadius: posterRadius }] }>
{/* 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 ? (
<ExpoImage
source={{ uri: optimizedPosterUrl }}
style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, borderRadius: posterRadius }]}
contentFit="cover"
cachePolicy={Platform.OS === 'android' ? 'disk' : 'memory-disk'}
transition={140}
transition={100} // Faster transition for scrolling
allowDownscaling
priority="low" // Deprioritize decode for long lists
priority="normal" // Normal priority for horizontal scrolling
onLoad={() => {
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
<View style={[styles.poster, { backgroundColor: currentTheme.colors.elevation1, justifyContent: 'center', alignItems: 'center', borderRadius: posterRadius }] }>
<Text style={{ color: currentTheme.colors.textMuted, fontSize: 10, textAlign: 'center' }}>
{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;
});

View file

@ -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);
}
});

View file

@ -588,6 +588,20 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ 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<CatalogScreenProps> = ({ route, navigation }) => {
activeOpacity={0.7}
>
<Image
source={{ uri: item.poster || 'https://via.placeholder.com/300x450/333333/666666?text=No+Image' }}
source={{ uri: optimizePosterUrl(item.poster) }}
style={styles.poster}
contentFit="cover"
cachePolicy="disk"
transition={0}
cachePolicy={Platform.OS === 'android' ? 'memory-disk' : 'memory-disk'}
transition={100}
allowDownscaling
priority="normal"
recyclingKey={`${item.id}-${item.type}`}
/>
{type === 'movie' && nowPlayingMovies.has(item.id) && (
@ -644,7 +660,7 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
)}
</TouchableOpacity>
);
}, [navigation, styles, effectiveNumColumns, effectiveItemWidth, type, nowPlayingMovies]);
}, [navigation, styles, effectiveNumColumns, effectiveItemWidth, type, nowPlayingMovies, colors.white, optimizePosterUrl]);
const renderEmptyState = () => (
<View style={styles.centered}>
@ -754,6 +770,11 @@ const CatalogScreen: React.FC<CatalogScreenProps> = ({ route, navigation }) => {
}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
removeClippedSubviews={true}
maxToRenderPerBatch={effectiveNumColumns * 3}
windowSize={10}
initialNumToRender={effectiveNumColumns * 4}
getItemType={() => 'item'}
/>
) : renderEmptyState()}
</SafeAreaView>