mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-20 16:22:04 +00:00
Refactor HomeScreen to use FlatList for improved performance and structure
This update replaces the ScrollView with a FlatList in the HomeScreen component, enhancing the rendering of catalog items and improving performance. The list data structure is also updated to include placeholders for loading states, ensuring a smoother user experience. Additionally, the logic for rendering list items and the footer component is optimized for better clarity and maintainability.
This commit is contained in:
parent
5f8bb8948d
commit
152ec88e04
1 changed files with 125 additions and 380 deletions
|
|
@ -73,299 +73,16 @@ interface Category {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContentItemProps {
|
|
||||||
item: StreamingContent;
|
|
||||||
onPress: (id: string, type: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DropUpMenuProps {
|
|
||||||
visible: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
item: StreamingContent;
|
|
||||||
onOptionSelect: (option: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContinueWatchingRef {
|
interface ContinueWatchingRef {
|
||||||
refresh: () => Promise<boolean>;
|
refresh: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DropUpMenu = React.memo(({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => {
|
type HomeScreenListItem =
|
||||||
const translateY = useSharedValue(300);
|
| { type: 'featured'; key: string }
|
||||||
const opacity = useSharedValue(0);
|
| { type: 'thisWeek'; key: string }
|
||||||
const isDarkMode = useColorScheme() === 'dark';
|
| { type: 'continueWatching'; key: string }
|
||||||
const { currentTheme } = useTheme();
|
| { type: 'catalog'; catalog: CatalogContent; key: string }
|
||||||
const SNAP_THRESHOLD = 100;
|
| { type: 'placeholder'; key: string };
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (visible) {
|
|
||||||
opacity.value = withTiming(1, { duration: 200 });
|
|
||||||
translateY.value = withTiming(0, { duration: 300 });
|
|
||||||
} else {
|
|
||||||
opacity.value = withTiming(0, { duration: 200 });
|
|
||||||
translateY.value = withTiming(300, { duration: 300 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup animations when component unmounts
|
|
||||||
return () => {
|
|
||||||
opacity.value = 0;
|
|
||||||
translateY.value = 300;
|
|
||||||
};
|
|
||||||
}, [visible]);
|
|
||||||
|
|
||||||
const gesture = useMemo(() => Gesture.Pan()
|
|
||||||
.onStart(() => {
|
|
||||||
// Store initial position if needed
|
|
||||||
})
|
|
||||||
.onUpdate((event) => {
|
|
||||||
if (event.translationY > 0) { // Only allow dragging downwards
|
|
||||||
translateY.value = event.translationY;
|
|
||||||
opacity.value = interpolate(
|
|
||||||
event.translationY,
|
|
||||||
[0, 300],
|
|
||||||
[1, 0],
|
|
||||||
Extrapolate.CLAMP
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.onEnd((event) => {
|
|
||||||
if (event.translationY > SNAP_THRESHOLD || event.velocityY > 500) {
|
|
||||||
translateY.value = withTiming(300, { duration: 300 });
|
|
||||||
opacity.value = withTiming(0, { duration: 200 });
|
|
||||||
runOnJS(onClose)();
|
|
||||||
} else {
|
|
||||||
translateY.value = withTiming(0, { duration: 300 });
|
|
||||||
opacity.value = withTiming(1, { duration: 200 });
|
|
||||||
}
|
|
||||||
}), [onClose]);
|
|
||||||
|
|
||||||
const overlayStyle = useAnimatedStyle(() => ({
|
|
||||||
opacity: opacity.value,
|
|
||||||
backgroundColor: currentTheme.colors.transparentDark,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const menuStyle = useAnimatedStyle(() => ({
|
|
||||||
transform: [{ translateY: translateY.value }],
|
|
||||||
borderTopLeftRadius: 24,
|
|
||||||
borderTopRightRadius: 24,
|
|
||||||
backgroundColor: isDarkMode ? currentTheme.colors.elevation2 : currentTheme.colors.white,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const menuOptions = useMemo(() => [
|
|
||||||
{
|
|
||||||
icon: item.inLibrary ? 'bookmark' : 'bookmark-border',
|
|
||||||
label: item.inLibrary ? 'Remove from Library' : 'Add to Library',
|
|
||||||
action: 'library'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'check-circle',
|
|
||||||
label: 'Mark as Watched',
|
|
||||||
action: 'watched'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'playlist-add',
|
|
||||||
label: 'Add to Playlist',
|
|
||||||
action: 'playlist'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: 'share',
|
|
||||||
label: 'Share',
|
|
||||||
action: 'share'
|
|
||||||
}
|
|
||||||
], [item.inLibrary]);
|
|
||||||
|
|
||||||
const handleOptionSelect = useCallback((action: string) => {
|
|
||||||
onOptionSelect(action);
|
|
||||||
onClose();
|
|
||||||
}, [onOptionSelect, onClose]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
visible={visible}
|
|
||||||
transparent
|
|
||||||
animationType="none"
|
|
||||||
onRequestClose={onClose}
|
|
||||||
>
|
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
||||||
<Animated.View style={[styles.modalOverlay, overlayStyle]}>
|
|
||||||
<Pressable style={styles.modalOverlayPressable} onPress={onClose} />
|
|
||||||
<GestureDetector gesture={gesture}>
|
|
||||||
<Animated.View style={[styles.menuContainer, menuStyle]}>
|
|
||||||
<View style={[styles.dragHandle, { backgroundColor: currentTheme.colors.transparentLight }]} />
|
|
||||||
<View style={[styles.menuHeader, { borderBottomColor: currentTheme.colors.border }]}>
|
|
||||||
<ExpoImage
|
|
||||||
source={{ uri: item.poster }}
|
|
||||||
style={styles.menuPoster}
|
|
||||||
contentFit="cover"
|
|
||||||
/>
|
|
||||||
<View style={styles.menuTitleContainer}>
|
|
||||||
<Text style={[styles.menuTitle, { color: isDarkMode ? currentTheme.colors.white : currentTheme.colors.black }]}>
|
|
||||||
{item.name}
|
|
||||||
</Text>
|
|
||||||
{item.year && (
|
|
||||||
<Text style={[styles.menuYear, { color: isDarkMode ? currentTheme.colors.mediumEmphasis : currentTheme.colors.textMutedDark }]}>
|
|
||||||
{item.year}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View style={styles.menuOptions}>
|
|
||||||
{menuOptions.map((option, index) => (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={option.action}
|
|
||||||
style={[
|
|
||||||
styles.menuOption,
|
|
||||||
{ borderBottomColor: isDarkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)' },
|
|
||||||
index === menuOptions.length - 1 && styles.lastMenuOption
|
|
||||||
]}
|
|
||||||
onPress={() => handleOptionSelect(option.action)}
|
|
||||||
>
|
|
||||||
<MaterialIcons
|
|
||||||
name={option.icon as "bookmark" | "check-circle" | "playlist-add" | "share" | "bookmark-border"}
|
|
||||||
size={24}
|
|
||||||
color={currentTheme.colors.primary}
|
|
||||||
/>
|
|
||||||
<Text style={[
|
|
||||||
styles.menuOptionText,
|
|
||||||
{ color: isDarkMode ? currentTheme.colors.white : currentTheme.colors.black }
|
|
||||||
]}>
|
|
||||||
{option.label}
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</Animated.View>
|
|
||||||
</GestureDetector>
|
|
||||||
</Animated.View>
|
|
||||||
</GestureHandlerRootView>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const ContentItem = React.memo(({ item: initialItem, onPress }: ContentItemProps) => {
|
|
||||||
const [menuVisible, setMenuVisible] = useState(false);
|
|
||||||
const [localItem, setLocalItem] = useState(initialItem);
|
|
||||||
const [isWatched, setIsWatched] = useState(false);
|
|
||||||
const [imageLoaded, setImageLoaded] = useState(false);
|
|
||||||
const [imageError, setImageError] = useState(false);
|
|
||||||
const { currentTheme } = useTheme();
|
|
||||||
|
|
||||||
const handleLongPress = useCallback(() => {
|
|
||||||
setMenuVisible(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handlePress = useCallback(() => {
|
|
||||||
onPress(localItem.id, localItem.type);
|
|
||||||
}, [localItem.id, localItem.type, onPress]);
|
|
||||||
|
|
||||||
const handleOptionSelect = useCallback((option: string) => {
|
|
||||||
switch (option) {
|
|
||||||
case 'library':
|
|
||||||
if (localItem.inLibrary) {
|
|
||||||
catalogService.removeFromLibrary(localItem.type, localItem.id);
|
|
||||||
} else {
|
|
||||||
catalogService.addToLibrary(localItem);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'watched':
|
|
||||||
setIsWatched(prev => !prev);
|
|
||||||
break;
|
|
||||||
case 'playlist':
|
|
||||||
case 'share':
|
|
||||||
// These options don't have implementations yet
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}, [localItem]);
|
|
||||||
|
|
||||||
const handleMenuClose = useCallback(() => {
|
|
||||||
setMenuVisible(false);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Only update localItem when initialItem changes
|
|
||||||
useEffect(() => {
|
|
||||||
setLocalItem(initialItem);
|
|
||||||
}, [initialItem]);
|
|
||||||
|
|
||||||
// Subscribe to library updates
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribe = catalogService.subscribeToLibraryUpdates((libraryItems) => {
|
|
||||||
const isInLibrary = libraryItems.some(
|
|
||||||
libraryItem => libraryItem.id === localItem.id && libraryItem.type === localItem.type
|
|
||||||
);
|
|
||||||
if (isInLibrary !== localItem.inLibrary) {
|
|
||||||
setLocalItem(prev => ({ ...prev, inLibrary: isInLibrary }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => unsubscribe();
|
|
||||||
}, [localItem.id, localItem.type]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.contentItem}
|
|
||||||
activeOpacity={0.7}
|
|
||||||
onPress={handlePress}
|
|
||||||
onLongPress={handleLongPress}
|
|
||||||
delayLongPress={300}
|
|
||||||
>
|
|
||||||
<View style={styles.contentItemContainer}>
|
|
||||||
<ExpoImage
|
|
||||||
source={{ uri: localItem.poster }}
|
|
||||||
style={styles.poster}
|
|
||||||
contentFit="cover"
|
|
||||||
transition={300}
|
|
||||||
cachePolicy="memory-disk"
|
|
||||||
recyclingKey={`poster-${localItem.id}`}
|
|
||||||
onLoadStart={() => {
|
|
||||||
setImageLoaded(false);
|
|
||||||
setImageError(false);
|
|
||||||
}}
|
|
||||||
onLoadEnd={() => setImageLoaded(true)}
|
|
||||||
onError={() => {
|
|
||||||
setImageError(true);
|
|
||||||
setImageLoaded(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{(!imageLoaded || imageError) && (
|
|
||||||
<View style={[styles.loadingOverlay, { backgroundColor: currentTheme.colors.elevation2 }]}>
|
|
||||||
{!imageError ? (
|
|
||||||
<ActivityIndicator color={currentTheme.colors.primary} size="small" />
|
|
||||||
) : (
|
|
||||||
<MaterialIcons name="broken-image" size={24} color={currentTheme.colors.lightGray} />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
{isWatched && (
|
|
||||||
<View style={styles.watchedIndicator}>
|
|
||||||
<MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
{localItem.inLibrary && (
|
|
||||||
<View style={styles.libraryBadge}>
|
|
||||||
<MaterialIcons name="bookmark" size={16} color={currentTheme.colors.white} />
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
{menuVisible && (
|
|
||||||
<DropUpMenu
|
|
||||||
visible={menuVisible}
|
|
||||||
onClose={handleMenuClose}
|
|
||||||
item={localItem}
|
|
||||||
onOptionSelect={handleOptionSelect}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}, (prevProps, nextProps) => {
|
|
||||||
// Custom comparison function to prevent unnecessary re-renders
|
|
||||||
return (
|
|
||||||
prevProps.item.id === nextProps.item.id &&
|
|
||||||
prevProps.item.inLibrary === nextProps.item.inLibrary &&
|
|
||||||
prevProps.onPress === nextProps.onPress
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sample categories (real app would get these from API)
|
// Sample categories (real app would get these from API)
|
||||||
const SAMPLE_CATEGORIES: Category[] = [
|
const SAMPLE_CATEGORIES: Category[] = [
|
||||||
|
|
@ -397,7 +114,7 @@ const HomeScreen = () => {
|
||||||
const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
const refreshTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const [hasContinueWatching, setHasContinueWatching] = useState(false);
|
const [hasContinueWatching, setHasContinueWatching] = useState(false);
|
||||||
|
|
||||||
const [catalogs, setCatalogs] = useState<CatalogContent[]>([]);
|
const [catalogs, setCatalogs] = useState<(CatalogContent | null)[]>([]);
|
||||||
const [catalogsLoading, setCatalogsLoading] = useState(true);
|
const [catalogsLoading, setCatalogsLoading] = useState(true);
|
||||||
const [loadedCatalogCount, setLoadedCatalogCount] = useState(0);
|
const [loadedCatalogCount, setLoadedCatalogCount] = useState(0);
|
||||||
const totalCatalogsRef = useRef(0);
|
const totalCatalogsRef = useRef(0);
|
||||||
|
|
@ -423,6 +140,9 @@ const HomeScreen = () => {
|
||||||
const catalogSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY);
|
const catalogSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY);
|
||||||
const catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {};
|
const catalogSettings = catalogSettingsJson ? JSON.parse(catalogSettingsJson) : {};
|
||||||
|
|
||||||
|
// Hoist addon manifest loading out of the loop
|
||||||
|
const addonManifests = await stremioService.getInstalledAddonsAsync();
|
||||||
|
|
||||||
// Create placeholder array with proper order and track indices
|
// Create placeholder array with proper order and track indices
|
||||||
const catalogPlaceholders: (CatalogContent | null)[] = [];
|
const catalogPlaceholders: (CatalogContent | null)[] = [];
|
||||||
const catalogPromises: Promise<void>[] = [];
|
const catalogPromises: Promise<void>[] = [];
|
||||||
|
|
@ -442,8 +162,7 @@ const HomeScreen = () => {
|
||||||
|
|
||||||
const catalogPromise = (async () => {
|
const catalogPromise = (async () => {
|
||||||
try {
|
try {
|
||||||
const addonManifest = await stremioService.getInstalledAddonsAsync();
|
const manifest = addonManifests.find((a: any) => a.id === addon.id);
|
||||||
const manifest = addonManifest.find((a: any) => a.id === addon.id);
|
|
||||||
if (!manifest) return;
|
if (!manifest) return;
|
||||||
|
|
||||||
const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1);
|
const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1);
|
||||||
|
|
@ -737,6 +456,106 @@ const HomeScreen = () => {
|
||||||
return null;
|
return null;
|
||||||
}, [isLoading, currentTheme.colors]);
|
}, [isLoading, currentTheme.colors]);
|
||||||
|
|
||||||
|
const listData: HomeScreenListItem[] = useMemo(() => {
|
||||||
|
const data: HomeScreenListItem[] = [];
|
||||||
|
|
||||||
|
if (showHeroSection) {
|
||||||
|
data.push({ type: 'featured', key: 'featured' });
|
||||||
|
}
|
||||||
|
|
||||||
|
data.push({ type: 'thisWeek', key: 'thisWeek' });
|
||||||
|
data.push({ type: 'continueWatching', key: 'continueWatching' });
|
||||||
|
|
||||||
|
catalogs.forEach((catalog, index) => {
|
||||||
|
if (catalog) {
|
||||||
|
data.push({ type: 'catalog', catalog, key: `${catalog.addon}-${catalog.id}-${index}` });
|
||||||
|
} else {
|
||||||
|
// Add a key for placeholders
|
||||||
|
data.push({ type: 'placeholder', key: `placeholder-${index}` });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}, [showHeroSection, catalogs]);
|
||||||
|
|
||||||
|
const renderListItem = useCallback(({ item }: { item: HomeScreenListItem }) => {
|
||||||
|
switch (item.type) {
|
||||||
|
case 'featured':
|
||||||
|
return (
|
||||||
|
<FeaturedContent
|
||||||
|
key={`featured-${showHeroSection}-${featuredContentSource}`}
|
||||||
|
featuredContent={featuredContent}
|
||||||
|
isSaved={isSaved}
|
||||||
|
handleSaveToLibrary={handleSaveToLibrary}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'thisWeek':
|
||||||
|
return <Animated.View entering={FadeIn.duration(400).delay(150)}><ThisWeekSection /></Animated.View>;
|
||||||
|
case 'continueWatching':
|
||||||
|
return <ContinueWatchingSection ref={continueWatchingRef} />;
|
||||||
|
case 'catalog':
|
||||||
|
return (
|
||||||
|
<Animated.View entering={FadeIn.duration(300)}>
|
||||||
|
<CatalogSection catalog={item.catalog} />
|
||||||
|
</Animated.View>
|
||||||
|
);
|
||||||
|
case 'placeholder':
|
||||||
|
return (
|
||||||
|
<View style={styles.catalogPlaceholder}>
|
||||||
|
<View style={styles.placeholderHeader}>
|
||||||
|
<View style={[styles.placeholderTitle, { backgroundColor: currentTheme.colors.elevation1 }]} />
|
||||||
|
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.placeholderPosters}>
|
||||||
|
{[...Array(4)].map((_, posterIndex) => (
|
||||||
|
<View
|
||||||
|
key={posterIndex}
|
||||||
|
style={[styles.placeholderPoster, { backgroundColor: currentTheme.colors.elevation1 }]}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
showHeroSection,
|
||||||
|
featuredContentSource,
|
||||||
|
featuredContent,
|
||||||
|
isSaved,
|
||||||
|
handleSaveToLibrary,
|
||||||
|
currentTheme.colors
|
||||||
|
]);
|
||||||
|
|
||||||
|
const ListFooterComponent = useMemo(() => (
|
||||||
|
<>
|
||||||
|
{catalogsLoading && catalogs.length < totalCatalogsRef.current && (
|
||||||
|
<View style={styles.loadingMoreCatalogs}>
|
||||||
|
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
|
||||||
|
<Text style={[styles.loadingMoreText, { color: currentTheme.colors.textMuted }]}>
|
||||||
|
Loading more content... ({loadedCatalogCount}/{totalCatalogsRef.current})
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{!catalogsLoading && catalogs.filter(c => c).length === 0 && (
|
||||||
|
<View style={[styles.emptyCatalog, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
||||||
|
<MaterialIcons name="movie-filter" size={40} color={currentTheme.colors.textDark} />
|
||||||
|
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
|
||||||
|
No content available
|
||||||
|
</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
|
||||||
|
onPress={() => navigation.navigate('Settings')}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} />
|
||||||
|
<Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
), [catalogsLoading, catalogs, loadedCatalogCount, totalCatalogsRef.current, navigation, currentTheme.colors]);
|
||||||
|
|
||||||
// Memoize the main content section
|
// Memoize the main content section
|
||||||
const renderMainContent = useMemo(() => {
|
const renderMainContent = useMemo(() => {
|
||||||
if (isLoading) return null;
|
if (isLoading) return null;
|
||||||
|
|
@ -748,102 +567,28 @@ const HomeScreen = () => {
|
||||||
backgroundColor="transparent"
|
backgroundColor="transparent"
|
||||||
translucent
|
translucent
|
||||||
/>
|
/>
|
||||||
<ScrollView
|
<FlatList
|
||||||
|
data={listData}
|
||||||
|
renderItem={renderListItem}
|
||||||
|
keyExtractor={item => item.key}
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
styles.scrollContent,
|
styles.scrollContent,
|
||||||
{ paddingTop: Platform.OS === 'ios' ? 100 : 90 }
|
{ paddingTop: Platform.OS === 'ios' ? 100 : 90 }
|
||||||
]}
|
]}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
removeClippedSubviews={true}
|
ListFooterComponent={ListFooterComponent}
|
||||||
>
|
initialNumToRender={5}
|
||||||
{showHeroSection && (
|
maxToRenderPerBatch={5}
|
||||||
<FeaturedContent
|
windowSize={10}
|
||||||
key={`featured-${showHeroSection}-${featuredContentSource}`}
|
/>
|
||||||
featuredContent={featuredContent}
|
|
||||||
isSaved={isSaved}
|
|
||||||
handleSaveToLibrary={handleSaveToLibrary}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Animated.View entering={FadeIn.duration(400).delay(150)}>
|
|
||||||
<ThisWeekSection />
|
|
||||||
</Animated.View>
|
|
||||||
|
|
||||||
<ContinueWatchingSection ref={continueWatchingRef} />
|
|
||||||
|
|
||||||
{/* Show catalogs as they load */}
|
|
||||||
{catalogs.map((catalog, index) => {
|
|
||||||
if (!catalog) {
|
|
||||||
// Show placeholder for loading catalog
|
|
||||||
return (
|
|
||||||
<View key={`placeholder-${index}`} style={styles.catalogPlaceholder}>
|
|
||||||
<View style={styles.placeholderHeader}>
|
|
||||||
<View style={[styles.placeholderTitle, { backgroundColor: currentTheme.colors.elevation1 }]} />
|
|
||||||
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
|
|
||||||
</View>
|
|
||||||
<View style={styles.placeholderPosters}>
|
|
||||||
{[...Array(4)].map((_, posterIndex) => (
|
|
||||||
<View
|
|
||||||
key={posterIndex}
|
|
||||||
style={[styles.placeholderPoster, { backgroundColor: currentTheme.colors.elevation1 }]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Animated.View
|
|
||||||
key={`${catalog.addon}-${catalog.id}-${index}`}
|
|
||||||
entering={FadeIn.duration(300)}
|
|
||||||
>
|
|
||||||
<CatalogSection catalog={catalog} />
|
|
||||||
</Animated.View>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{/* Show loading indicator for remaining catalogs */}
|
|
||||||
{catalogsLoading && catalogs.length < totalCatalogsRef.current && (
|
|
||||||
<View style={styles.loadingMoreCatalogs}>
|
|
||||||
<ActivityIndicator size="small" color={currentTheme.colors.primary} />
|
|
||||||
<Text style={[styles.loadingMoreText, { color: currentTheme.colors.textMuted }]}>
|
|
||||||
Loading more content... ({loadedCatalogCount}/{totalCatalogsRef.current})
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Show empty state only if all catalogs are loaded and none are available */}
|
|
||||||
{!catalogsLoading && catalogs.length === 0 && (
|
|
||||||
<View style={[styles.emptyCatalog, { backgroundColor: currentTheme.colors.elevation1 }]}>
|
|
||||||
<MaterialIcons name="movie-filter" size={40} color={currentTheme.colors.textDark} />
|
|
||||||
<Text style={{ color: currentTheme.colors.textDark, marginTop: 8, fontSize: 16, textAlign: 'center' }}>
|
|
||||||
No content available
|
|
||||||
</Text>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.addCatalogButton, { backgroundColor: currentTheme.colors.primary }]}
|
|
||||||
onPress={() => navigation.navigate('Settings')}
|
|
||||||
>
|
|
||||||
<MaterialIcons name="add-circle" size={20} color={currentTheme.colors.white} />
|
|
||||||
<Text style={[styles.addCatalogButtonText, { color: currentTheme.colors.white }]}>Add Catalogs</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
isLoading,
|
isLoading,
|
||||||
currentTheme.colors,
|
currentTheme.colors,
|
||||||
showHeroSection,
|
listData,
|
||||||
featuredContent,
|
renderListItem,
|
||||||
isSaved,
|
ListFooterComponent
|
||||||
handleSaveToLibrary,
|
|
||||||
hasContinueWatching,
|
|
||||||
catalogs,
|
|
||||||
catalogsLoading,
|
|
||||||
navigation,
|
|
||||||
featuredContentSource
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return isLoading ? renderLoadingScreen : renderMainContent;
|
return isLoading ? renderLoadingScreen : renderMainContent;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue