import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator, SafeAreaView, StatusBar, RefreshControl, Dimensions, } from 'react-native'; import { RouteProp } from '@react-navigation/native'; import { StackNavigationProp } from '@react-navigation/stack'; import { RootStackParamList } from '../navigation/AppNavigator'; import { Meta, stremioService } from '../services/stremioService'; import { colors } from '../styles'; import { Image } from 'expo-image'; import { logger } from '../utils/logger'; type CatalogScreenProps = { route: RouteProp; navigation: StackNavigationProp; }; // Consistent spacing variables const SPACING = { xs: 4, sm: 8, md: 12, lg: 16, xl: 24, }; // Screen dimensions and grid layout const { width } = Dimensions.get('window'); const NUM_COLUMNS = 3; const ITEM_MARGIN = SPACING.sm; const ITEM_WIDTH = (width - (SPACING.md * 2) - (ITEM_MARGIN * 2 * NUM_COLUMNS)) / NUM_COLUMNS; const CatalogScreen: React.FC = ({ route, navigation }) => { const { addonId, type, id, name, genreFilter } = route.params; const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [refreshing, setRefreshing] = useState(false); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [error, setError] = useState(null); // Force dark mode instead of using color scheme const isDarkMode = true; const loadItems = useCallback(async (pageNum: number, shouldRefresh: boolean = false) => { try { if (shouldRefresh) { setRefreshing(true); } else if (pageNum === 1) { setLoading(true); } setError(null); // Use this flag to track if we found and processed any items let foundItems = false; let allItems: Meta[] = []; // Get all installed addon manifests directly const manifests = await stremioService.getInstalledAddonsAsync(); if (addonId) { // If addon ID is provided, find the specific addon const addon = manifests.find(a => a.id === addonId); if (!addon) { throw new Error(`Addon ${addonId} not found`); } // Create filters array for genre filtering if provided const filters = genreFilter ? [{ title: 'genre', value: genreFilter }] : []; // Load items from the catalog const newItems = await stremioService.getCatalog(addon, type, id, pageNum, filters); if (newItems.length === 0) { setHasMore(false); } else { foundItems = true; } if (shouldRefresh || pageNum === 1) { setItems(newItems); } else { setItems(prev => [...prev, ...newItems]); } } else if (genreFilter) { // Get all addons that have catalogs of the specified type const typeManifests = manifests.filter(manifest => manifest.catalogs && manifest.catalogs.some(catalog => catalog.type === type) ); // For each addon, try to get content with the genre filter for (const manifest of typeManifests) { try { // Find catalogs of this type const typeCatalogs = manifest.catalogs?.filter(catalog => catalog.type === type) || []; // For each catalog, try to get content for (const catalog of typeCatalogs) { try { const filters = [{ title: 'genre', value: genreFilter }]; const catalogItems = await stremioService.getCatalog(manifest, type, catalog.id, pageNum, filters); if (catalogItems && catalogItems.length > 0) { allItems = [...allItems, ...catalogItems]; foundItems = true; } } catch (error) { logger.log(`Failed to load items from ${manifest.name} catalog ${catalog.id}:`, error); // Continue with other catalogs } } } catch (error) { logger.log(`Failed to process addon ${manifest.name}:`, error); // Continue with other addons } } // Remove duplicates by ID const uniqueItems = allItems.filter((item, index, self) => index === self.findIndex((t) => t.id === item.id) ); if (uniqueItems.length === 0) { setHasMore(false); } if (shouldRefresh || pageNum === 1) { setItems(uniqueItems); } else { // Add new items while avoiding duplicates setItems(prev => { const prevIds = new Set(prev.map(item => item.id)); const newItems = uniqueItems.filter(item => !prevIds.has(item.id)); return [...prev, ...newItems]; }); } } if (!foundItems) { setError("No content found for the selected filters"); } } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load catalog items'); logger.error('Failed to load catalog:', err); } finally { setLoading(false); setRefreshing(false); } }, [addonId, type, id, genreFilter]); useEffect(() => { loadItems(1); // Set the header title navigation.setOptions({ title: name || `${type} catalog` }); }, [loadItems, navigation, name, type]); const handleRefresh = useCallback(() => { setPage(1); loadItems(1, true); }, [loadItems]); const handleLoadMore = useCallback(() => { if (!loading && hasMore) { const nextPage = page + 1; setPage(nextPage); loadItems(nextPage); } }, [loading, hasMore, page, loadItems]); const renderItem = useCallback(({ item }: { item: Meta }) => { return ( navigation.navigate('Metadata', { id: item.id, type: item.type })} activeOpacity={0.7} > {item.name} {item.releaseInfo && ( {item.releaseInfo} )} ); }, [navigation]); const renderEmptyState = () => ( No content found for the selected genre Try Again ); const renderErrorState = () => ( {error} loadItems(1)} > Retry ); const renderLoadingState = () => ( ); if (loading && items.length === 0) { return ( {renderLoadingState()} ); } if (error && items.length === 0) { return ( {renderErrorState()} ); } return ( {items.length > 0 ? ( `${item.id}-${item.type}`} numColumns={NUM_COLUMNS} refreshControl={ } onEndReached={handleLoadMore} onEndReachedThreshold={0.5} ListFooterComponent={ loading && items.length > 0 ? ( ) : null } contentContainerStyle={styles.list} columnWrapperStyle={styles.columnWrapper} /> ) : renderEmptyState()} ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.darkBackground, }, list: { padding: SPACING.md, }, columnWrapper: { justifyContent: 'space-between', }, item: { width: ITEM_WIDTH, marginBottom: SPACING.md, borderRadius: 8, overflow: 'hidden', }, poster: { width: '100%', aspectRatio: 2/3, borderRadius: 8, backgroundColor: colors.transparentLight, }, itemContent: { padding: SPACING.xs, }, title: { marginTop: SPACING.xs, fontSize: 14, fontWeight: '600', color: colors.white, lineHeight: 18, }, releaseInfo: { fontSize: 12, marginTop: SPACING.xs, color: colors.lightGray, }, footer: { padding: SPACING.lg, alignItems: 'center', }, button: { marginTop: SPACING.md, paddingVertical: SPACING.md, paddingHorizontal: SPACING.xl, backgroundColor: colors.primary, borderRadius: 8, elevation: 2, }, buttonText: { color: colors.white, fontWeight: '600', fontSize: 16, }, centered: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: SPACING.xl, }, emptyText: { color: colors.white, fontSize: 16, textAlign: 'center', marginBottom: SPACING.md, }, errorText: { color: colors.white, fontSize: 16, textAlign: 'center', marginBottom: SPACING.md, }, }); export default CatalogScreen;