mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
some UI optimization
This commit is contained in:
parent
57036aaffb
commit
b43957e6f9
5 changed files with 86 additions and 96 deletions
48
App.tsx
48
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 (
|
||||
<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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue