import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator, SafeAreaView, StatusBar, Dimensions, ScrollView, Platform, Animated, } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import { colors } from '../styles'; import { catalogService, StreamingContent, CatalogContent } from '../services/catalogService'; import { Image } from 'expo-image'; import { FadeIn, FadeOut, SlideInRight, Layout } from 'react-native-reanimated'; import { LinearGradient } from 'expo-linear-gradient'; import { RootStackParamList } from '../navigation/AppNavigator'; import { logger } from '../utils/logger'; import { BlurView } from 'expo-blur'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; interface Category { id: string; name: string; type: 'movie' | 'series' | 'channel' | 'tv'; icon: keyof typeof MaterialIcons.glyphMap; } interface GenreCatalog { genre: string; items: StreamingContent[]; } const CATEGORIES: Category[] = [ { id: 'movie', name: 'Movies', type: 'movie', icon: 'local-movies' }, { id: 'series', name: 'TV Shows', type: 'series', icon: 'live-tv' } ]; // Common genres for movies and TV shows const COMMON_GENRES = [ 'All', 'Action', 'Adventure', 'Animation', 'Comedy', 'Crime', 'Documentary', 'Drama', 'Family', 'Fantasy', 'History', 'Horror', 'Music', 'Mystery', 'Romance', 'Science Fiction', 'Thriller', 'War', 'Western' ]; const ANDROID_STATUSBAR_HEIGHT = StatusBar.currentHeight || 0; // Memoized child components const CategoryButton = React.memo(({ category, isSelected, onPress }: { category: Category; isSelected: boolean; onPress: () => void; }) => { const styles = useStyles(); return ( {category.name} ); }); const GenreButton = React.memo(({ genre, isSelected, onPress }: { genre: string; isSelected: boolean; onPress: () => void; }) => { const styles = useStyles(); return ( {genre} ); }); const ContentItem = React.memo(({ item, onPress }: { item: StreamingContent; onPress: () => void; }) => { const styles = useStyles(); const { width } = Dimensions.get('window'); const itemWidth = (width - 48) / 2.2; // 2 items per row with spacing return ( {item.name} {item.year && ( {item.year} )} ); }); const CatalogSection = React.memo(({ catalog, selectedCategory, navigation }: { catalog: GenreCatalog; selectedCategory: Category; navigation: NavigationProp; }) => { const styles = useStyles(); const { width } = Dimensions.get('window'); const itemWidth = (width - 48) / 2.2; // 2 items per row with spacing const displayItems = useMemo(() => catalog.items.slice(0, 3), [catalog.items] ); const handleContentPress = useCallback((item: StreamingContent) => { navigation.navigate('Metadata', { id: item.id, type: item.type }); }, [navigation]); const renderItem = useCallback(({ item }: { item: StreamingContent }) => ( handleContentPress(item)} /> ), [handleContentPress]); const handleSeeMorePress = useCallback(() => { // Get addon/catalog info from the first item (assuming homogeneity) const firstItem = catalog.items[0]; if (!firstItem) return; // Should not happen if section exists // We need addonId and catalogId. These aren't directly on StreamingContent. // We might need to fetch this or adjust the GenreCatalog structure. // FOR NOW: Assuming CatalogScreen can handle potentially missing addonId/catalogId // OR: We could pass the *genre* as the name and let CatalogScreen figure it out? // Let's pass the necessary info if available, assuming StreamingContent might have it // (Requires checking StreamingContent interface or how it's populated) // --- TEMPORARY/PLACEHOLDER --- // Ideally, GenreCatalog should contain addonId/catalogId for the group. // If not, CatalogScreen needs modification or we fetch IDs here. // Let's stick to passing genre and type for now, CatalogScreen logic might suffice? navigation.navigate('Catalog', { // Don't pass an addonId since we want to filter by genre across all addons id: catalog.genre, type: selectedCategory.type, name: `${catalog.genre} ${selectedCategory.name}`, genreFilter: catalog.genre // This will trigger the genre-based filtering logic in CatalogScreen }); // --- END TEMPORARY --- }, [navigation, selectedCategory, catalog.genre, catalog.items]); const keyExtractor = useCallback((item: StreamingContent) => item.id, []); const ItemSeparator = useCallback(() => , []); return ( {catalog.genre} See All ); }); // Extract styles into a hook for better performance with dimensions const useStyles = () => { const { width } = Dimensions.get('window'); return StyleSheet.create({ container: { flex: 1, backgroundColor: colors.darkBackground, }, headerBackground: { position: 'absolute', top: 0, left: 0, right: 0, backgroundColor: colors.darkBackground, zIndex: 1, }, contentContainer: { flex: 1, backgroundColor: colors.darkBackground, }, header: { paddingHorizontal: 20, justifyContent: 'flex-end', paddingBottom: 8, backgroundColor: 'transparent', zIndex: 2, }, headerContent: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, headerTitle: { fontSize: 32, fontWeight: '800', color: colors.white, letterSpacing: 0.3, }, searchButton: { padding: 10, borderRadius: 24, backgroundColor: 'rgba(255,255,255,0.08)', }, categoryContainer: { paddingVertical: 20, borderBottomWidth: 1, borderBottomColor: 'rgba(255,255,255,0.05)', }, categoriesContent: { flexDirection: 'row', justifyContent: 'center', paddingHorizontal: 20, gap: 16, }, categoryButton: { paddingHorizontal: 20, paddingVertical: 14, borderRadius: 24, backgroundColor: 'rgba(255,255,255,0.05)', flexDirection: 'row', alignItems: 'center', gap: 10, flex: 1, maxWidth: 160, justifyContent: 'center', shadowColor: colors.black, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.15, shadowRadius: 8, elevation: 4, }, selectedCategoryButton: { backgroundColor: colors.primary, }, categoryText: { color: colors.mediumGray, fontWeight: '600', fontSize: 16, }, selectedCategoryText: { color: colors.white, fontWeight: '700', }, genreContainer: { paddingTop: 20, paddingBottom: 12, zIndex: 10, }, genresScrollView: { paddingHorizontal: 20, paddingBottom: 8, }, genreButton: { paddingHorizontal: 18, paddingVertical: 10, marginRight: 12, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.05)', shadowColor: colors.black, shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2, overflow: 'hidden', }, selectedGenreButton: { backgroundColor: colors.primary, }, genreText: { color: colors.mediumGray, fontWeight: '500', fontSize: 14, }, selectedGenreText: { color: colors.white, fontWeight: '600', }, loadingContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', }, catalogsContainer: { paddingVertical: 8, }, catalogContainer: { marginBottom: 32, }, catalogHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, marginBottom: 16, }, catalogTitleContainer: { flexDirection: 'column', }, catalogTitleBar: { width: 32, height: 3, backgroundColor: colors.primary, marginTop: 6, borderRadius: 2, }, catalogTitle: { fontSize: 20, fontWeight: '700', color: colors.white, }, seeAllButton: { flexDirection: 'row', alignItems: 'center', gap: 4, paddingVertical: 6, paddingHorizontal: 4, }, seeAllText: { color: colors.primary, fontWeight: '600', fontSize: 14, }, contentItem: { marginHorizontal: 0, }, posterContainer: { borderRadius: 16, overflow: 'hidden', backgroundColor: 'rgba(255,255,255,0.03)', elevation: 5, shadowColor: colors.black, shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8, }, poster: { aspectRatio: 2/3, width: '100%', }, posterGradient: { position: 'absolute', bottom: 0, left: 0, right: 0, padding: 16, justifyContent: 'flex-end', height: '45%', }, contentTitle: { fontSize: 15, fontWeight: '700', color: colors.white, marginBottom: 4, textShadowColor: 'rgba(0, 0, 0, 0.75)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 2, letterSpacing: 0.3, }, contentYear: { fontSize: 12, color: 'rgba(255,255,255,0.7)', textShadowColor: 'rgba(0, 0, 0, 0.75)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 2, }, emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingTop: 80, }, emptyText: { color: colors.mediumGray, fontSize: 16, textAlign: 'center', paddingHorizontal: 32, }, }); }; const DiscoverScreen = () => { const navigation = useNavigation>(); const [selectedCategory, setSelectedCategory] = useState(CATEGORIES[0]); const [selectedGenre, setSelectedGenre] = useState('All'); const [catalogs, setCatalogs] = useState([]); const [allContent, setAllContent] = useState([]); const [loading, setLoading] = useState(true); const styles = useStyles(); const insets = useSafeAreaInsets(); // Force consistent status bar settings useEffect(() => { const applyStatusBarConfig = () => { StatusBar.setBarStyle('light-content'); if (Platform.OS === 'android') { StatusBar.setTranslucent(true); StatusBar.setBackgroundColor('transparent'); } }; applyStatusBarConfig(); // Re-apply on focus const unsubscribe = navigation.addListener('focus', applyStatusBarConfig); return unsubscribe; }, [navigation]); // Load content when category or genre changes useEffect(() => { loadContent(selectedCategory, selectedGenre); }, [selectedCategory, selectedGenre]); const loadContent = async (category: Category, genre: string) => { setLoading(true); try { // If genre is 'All', don't apply genre filter const genreFilter = genre === 'All' ? undefined : genre; const fetchedCatalogs = await catalogService.getCatalogByType(category.type, genreFilter); // Collect all content items const content: StreamingContent[] = []; fetchedCatalogs.forEach(catalog => { content.push(...catalog.items); }); setAllContent(content); if (genre === 'All') { // Group by genres when "All" is selected const genreCatalogs: GenreCatalog[] = []; // Get all genres from content const genresSet = new Set(); content.forEach(item => { if (item.genres && item.genres.length > 0) { item.genres.forEach(g => genresSet.add(g)); } }); // Create catalogs for each genre genresSet.forEach(g => { const genreItems = content.filter(item => item.genres && item.genres.includes(g) ); if (genreItems.length > 0) { genreCatalogs.push({ genre: g, items: genreItems }); } }); // Sort by number of items genreCatalogs.sort((a, b) => b.items.length - a.items.length); setCatalogs(genreCatalogs); } else { // When a specific genre is selected, show as a single catalog setCatalogs([{ genre, items: content }]); } } catch (error) { logger.error('Failed to load content:', error); setCatalogs([]); setAllContent([]); } finally { setLoading(false); } }; const handleCategoryPress = useCallback((category: Category) => { if (category.id !== selectedCategory.id) { setSelectedCategory(category); setSelectedGenre('All'); // Reset to All when changing category } }, [selectedCategory]); const handleGenrePress = useCallback((genre: string) => { if (genre !== selectedGenre) { setSelectedGenre(genre); } }, [selectedGenre]); const handleSearchPress = useCallback(() => { navigation.navigate('Search'); }, [navigation]); // Memoize rendering functions const renderCatalogItem = useCallback(({ item }: { item: GenreCatalog }) => ( ), [selectedCategory, navigation]); // Memoize list key extractor const catalogKeyExtractor = useCallback((item: GenreCatalog) => item.genre, []); const headerBaseHeight = Platform.OS === 'android' ? 80 : 60; const topSpacing = Platform.OS === 'android' ? (StatusBar.currentHeight || 0) : insets.top; const headerHeight = headerBaseHeight + topSpacing; return ( {/* Fixed position header background to prevent shifts */} {/* Header Section with proper top spacing */} Discover {/* Rest of the content */} {/* Categories Section */} {CATEGORIES.map((category) => ( handleCategoryPress(category)} /> ))} {/* Genres Section */} {COMMON_GENRES.map(genre => ( handleGenrePress(genre)} /> ))} {/* Content Section */} {loading ? ( ) : catalogs.length > 0 ? ( ) : ( No content found for {selectedGenre !== 'All' ? selectedGenre : 'these filters'} )} ); }; export default React.memo(DiscoverScreen);