diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index 39c179dd..16014831 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -73,299 +73,16 @@ interface Category { 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 { refresh: () => Promise; } -const DropUpMenu = React.memo(({ visible, onClose, item, onOptionSelect }: DropUpMenuProps) => { - const translateY = useSharedValue(300); - const opacity = useSharedValue(0); - const isDarkMode = useColorScheme() === 'dark'; - const { currentTheme } = useTheme(); - const SNAP_THRESHOLD = 100; - - 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 ( - - - - - - - - - - - - {item.name} - - {item.year && ( - - {item.year} - - )} - - - - {menuOptions.map((option, index) => ( - handleOptionSelect(option.action)} - > - - - {option.label} - - - ))} - - - - - - - ); -}); - -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 ( - <> - - - { - setImageLoaded(false); - setImageError(false); - }} - onLoadEnd={() => setImageLoaded(true)} - onError={() => { - setImageError(true); - setImageLoaded(true); - }} - /> - {(!imageLoaded || imageError) && ( - - {!imageError ? ( - - ) : ( - - )} - - )} - {isWatched && ( - - - - )} - {localItem.inLibrary && ( - - - - )} - - - - {menuVisible && ( - - )} - - ); -}, (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 - ); -}); +type HomeScreenListItem = + | { type: 'featured'; key: string } + | { type: 'thisWeek'; key: string } + | { type: 'continueWatching'; key: string } + | { type: 'catalog'; catalog: CatalogContent; key: string } + | { type: 'placeholder'; key: string }; // Sample categories (real app would get these from API) const SAMPLE_CATEGORIES: Category[] = [ @@ -397,7 +114,7 @@ const HomeScreen = () => { const refreshTimeoutRef = useRef(null); const [hasContinueWatching, setHasContinueWatching] = useState(false); - const [catalogs, setCatalogs] = useState([]); + const [catalogs, setCatalogs] = useState<(CatalogContent | null)[]>([]); const [catalogsLoading, setCatalogsLoading] = useState(true); const [loadedCatalogCount, setLoadedCatalogCount] = useState(0); const totalCatalogsRef = useRef(0); @@ -423,6 +140,9 @@ const HomeScreen = () => { const catalogSettingsJson = await AsyncStorage.getItem(CATALOG_SETTINGS_KEY); 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 const catalogPlaceholders: (CatalogContent | null)[] = []; const catalogPromises: Promise[] = []; @@ -442,8 +162,7 @@ const HomeScreen = () => { const catalogPromise = (async () => { try { - const addonManifest = await stremioService.getInstalledAddonsAsync(); - const manifest = addonManifest.find((a: any) => a.id === addon.id); + const manifest = addonManifests.find((a: any) => a.id === addon.id); if (!manifest) return; const metas = await stremioService.getCatalog(manifest, catalog.type, catalog.id, 1); @@ -737,6 +456,106 @@ const HomeScreen = () => { return null; }, [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 ( + + ); + case 'thisWeek': + return ; + case 'continueWatching': + return ; + case 'catalog': + return ( + + + + ); + case 'placeholder': + return ( + + + + + + + {[...Array(4)].map((_, posterIndex) => ( + + ))} + + + ); + default: + return null; + } + }, [ + showHeroSection, + featuredContentSource, + featuredContent, + isSaved, + handleSaveToLibrary, + currentTheme.colors + ]); + + const ListFooterComponent = useMemo(() => ( + <> + {catalogsLoading && catalogs.length < totalCatalogsRef.current && ( + + + + Loading more content... ({loadedCatalogCount}/{totalCatalogsRef.current}) + + + )} + {!catalogsLoading && catalogs.filter(c => c).length === 0 && ( + + + + No content available + + navigation.navigate('Settings')} + > + + Add Catalogs + + + )} + + ), [catalogsLoading, catalogs, loadedCatalogCount, totalCatalogsRef.current, navigation, currentTheme.colors]); + // Memoize the main content section const renderMainContent = useMemo(() => { if (isLoading) return null; @@ -748,102 +567,28 @@ const HomeScreen = () => { backgroundColor="transparent" translucent /> - item.key} contentContainerStyle={[ styles.scrollContent, { paddingTop: Platform.OS === 'ios' ? 100 : 90 } ]} showsVerticalScrollIndicator={false} - removeClippedSubviews={true} - > - {showHeroSection && ( - - )} - - - - - - - - {/* Show catalogs as they load */} - {catalogs.map((catalog, index) => { - if (!catalog) { - // Show placeholder for loading catalog - return ( - - - - - - - {[...Array(4)].map((_, posterIndex) => ( - - ))} - - - ); - } - - return ( - - - - ); - })} - - {/* Show loading indicator for remaining catalogs */} - {catalogsLoading && catalogs.length < totalCatalogsRef.current && ( - - - - Loading more content... ({loadedCatalogCount}/{totalCatalogsRef.current}) - - - )} - - {/* Show empty state only if all catalogs are loaded and none are available */} - {!catalogsLoading && catalogs.length === 0 && ( - - - - No content available - - navigation.navigate('Settings')} - > - - Add Catalogs - - - )} - + ListFooterComponent={ListFooterComponent} + initialNumToRender={5} + maxToRenderPerBatch={5} + windowSize={10} + /> ); }, [ - isLoading, - currentTheme.colors, - showHeroSection, - featuredContent, - isSaved, - handleSaveToLibrary, - hasContinueWatching, - catalogs, - catalogsLoading, - navigation, - featuredContentSource + isLoading, + currentTheme.colors, + listData, + renderListItem, + ListFooterComponent ]); return isLoading ? renderLoadingScreen : renderMainContent;