import React, { useState, useEffect, useCallback } from 'react'; import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator, SafeAreaView, StatusBar, RefreshControl, Dimensions, Platform, } 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 { MaterialIcons } from '@expo/vector-icons'; import { logger } from '../utils/logger'; type CatalogScreenProps = { route: RouteProp; navigation: StackNavigationProp; }; // Constants for layout const SPACING = { xs: 4, sm: 8, md: 12, lg: 16, xl: 24, }; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; // Screen dimensions and grid layout const { width } = Dimensions.get('window'); const NUM_COLUMNS = 3; const ITEM_MARGIN = SPACING.sm; const ITEM_WIDTH = (width - (SPACING.lg * 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 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); }, [loadItems]); 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 Try Again ); const renderErrorState = () => ( {error} loadItems(1)} > Retry ); const renderLoadingState = () => ( Loading content... ); if (loading && items.length === 0) { return ( navigation.goBack()} > Back {name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`} {renderLoadingState()} ); } if (error && items.length === 0) { return ( navigation.goBack()} > Back {name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`} {renderErrorState()} ); } return ( navigation.goBack()} > Back {name || `${type.charAt(0).toUpperCase() + type.slice(1)}s`} {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} showsVerticalScrollIndicator={false} /> ) : renderEmptyState()} ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.darkBackground, }, header: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingTop: Platform.OS === 'android' ? ANDROID_STATUSBAR_HEIGHT + 8 : 8, }, backButton: { flexDirection: 'row', alignItems: 'center', padding: 8, }, backText: { fontSize: 17, fontWeight: '400', color: colors.primary, }, headerTitle: { fontSize: 34, fontWeight: '700', color: colors.white, paddingHorizontal: 16, paddingBottom: 16, paddingTop: 8, }, list: { padding: SPACING.lg, paddingTop: SPACING.sm, }, columnWrapper: { justifyContent: 'space-between', }, item: { width: ITEM_WIDTH, marginBottom: SPACING.lg, borderRadius: 12, overflow: 'hidden', backgroundColor: colors.elevation2, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2, }, poster: { width: '100%', aspectRatio: 2/3, borderTopLeftRadius: 12, borderTopRightRadius: 12, backgroundColor: colors.elevation3, }, itemContent: { padding: SPACING.sm, }, title: { fontSize: 14, fontWeight: '600', color: colors.white, lineHeight: 18, }, releaseInfo: { fontSize: 12, marginTop: SPACING.xs, color: colors.mediumGray, }, 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', marginTop: SPACING.md, marginBottom: SPACING.sm, }, errorText: { color: colors.white, fontSize: 16, textAlign: 'center', marginTop: SPACING.md, marginBottom: SPACING.sm, }, loadingText: { color: colors.white, fontSize: 16, marginTop: SPACING.lg, } }); export default CatalogScreen;