Refactor DiscoverScreen component by removing unused imports and optimizing structure. Introduce CategorySelector and GenreSelector components for better organization. Update loading state handling and improve empty state rendering.

This commit is contained in:
tapframe 2025-05-03 15:56:00 +05:30
parent dbaadbe61b
commit cf03a44fab
8 changed files with 609 additions and 542 deletions

View file

@ -0,0 +1,132 @@
import React, { useCallback, useMemo } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, FlatList, Dimensions } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { colors } from '../../styles';
import { GenreCatalog, Category } from '../../constants/discover';
import { StreamingContent } from '../../services/catalogService';
import { RootStackParamList } from '../../navigation/AppNavigator';
import ContentItem from './ContentItem';
interface CatalogSectionProps {
catalog: GenreCatalog;
selectedCategory: Category;
}
const CatalogSection = ({ catalog, selectedCategory }: CatalogSectionProps) => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { width } = Dimensions.get('window');
const itemWidth = (width - 48) / 2.2; // 2 items per row with spacing
// Only display first 3 items in each section
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 handleSeeMorePress = useCallback(() => {
navigation.navigate('Catalog', {
id: catalog.genre,
type: selectedCategory.type,
name: `${catalog.genre} ${selectedCategory.name}`,
genreFilter: catalog.genre
});
}, [navigation, selectedCategory, catalog.genre]);
const renderItem = useCallback(({ item }: { item: StreamingContent }) => (
<ContentItem
item={item}
onPress={() => handleContentPress(item)}
width={itemWidth}
/>
), [handleContentPress, itemWidth]);
const keyExtractor = useCallback((item: StreamingContent) => item.id, []);
const ItemSeparator = useCallback(() => (
<View style={{ width: 16 }} />
), []);
return (
<View style={styles.container}>
<View style={styles.header}>
<View style={styles.titleContainer}>
<Text style={styles.title}>{catalog.genre}</Text>
<View style={styles.titleBar} />
</View>
<TouchableOpacity
onPress={handleSeeMorePress}
style={styles.seeAllButton}
activeOpacity={0.6}
>
<Text style={styles.seeAllText}>See All</Text>
<MaterialIcons name="arrow-forward-ios" color={colors.primary} size={14} />
</TouchableOpacity>
</View>
<FlatList
data={displayItems}
renderItem={renderItem}
keyExtractor={keyExtractor}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 16 }}
snapToInterval={itemWidth + 16}
decelerationRate="fast"
snapToAlignment="start"
ItemSeparatorComponent={ItemSeparator}
initialNumToRender={3}
maxToRenderPerBatch={3}
windowSize={3}
removeClippedSubviews={true}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
marginBottom: 32,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
marginBottom: 16,
},
titleContainer: {
flexDirection: 'column',
},
titleBar: {
width: 32,
height: 3,
backgroundColor: colors.primary,
marginTop: 6,
borderRadius: 2,
},
title: {
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,
},
});
export default React.memo(CatalogSection);

View file

@ -0,0 +1,43 @@
import React, { useCallback } from 'react';
import { FlatList, StyleSheet, Platform } from 'react-native';
import { GenreCatalog, Category } from '../../constants/discover';
import CatalogSection from './CatalogSection';
interface CatalogsListProps {
catalogs: GenreCatalog[];
selectedCategory: Category;
}
const CatalogsList = ({ catalogs, selectedCategory }: CatalogsListProps) => {
const renderCatalogItem = useCallback(({ item }: { item: GenreCatalog }) => (
<CatalogSection
catalog={item}
selectedCategory={selectedCategory}
/>
), [selectedCategory]);
// Memoize list key extractor
const catalogKeyExtractor = useCallback((item: GenreCatalog) => item.genre, []);
return (
<FlatList
data={catalogs}
renderItem={renderCatalogItem}
keyExtractor={catalogKeyExtractor}
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
initialNumToRender={3}
maxToRenderPerBatch={3}
windowSize={5}
removeClippedSubviews={Platform.OS === 'android'}
/>
);
};
const styles = StyleSheet.create({
container: {
paddingVertical: 8,
},
});
export default React.memo(CatalogsList);

View file

@ -0,0 +1,101 @@
import React, { useCallback } from 'react';
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { colors } from '../../styles';
import { Category } from '../../constants/discover';
interface CategorySelectorProps {
categories: Category[];
selectedCategory: Category;
onSelectCategory: (category: Category) => void;
}
const CategorySelector = ({
categories,
selectedCategory,
onSelectCategory
}: CategorySelectorProps) => {
const renderCategoryButton = useCallback((category: Category) => {
const isSelected = selectedCategory.id === category.id;
return (
<TouchableOpacity
key={category.id}
style={[
styles.categoryButton,
isSelected && styles.selectedCategoryButton
]}
onPress={() => onSelectCategory(category)}
activeOpacity={0.7}
>
<MaterialIcons
name={category.icon}
size={24}
color={isSelected ? colors.white : colors.mediumGray}
/>
<Text
style={[
styles.categoryText,
isSelected && styles.selectedCategoryText
]}
>
{category.name}
</Text>
</TouchableOpacity>
);
}, [selectedCategory, onSelectCategory]);
return (
<View style={styles.container}>
<View style={styles.content}>
{categories.map(renderCategoryButton)}
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
paddingVertical: 20,
borderBottomWidth: 1,
borderBottomColor: 'rgba(255,255,255,0.05)',
},
content: {
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',
},
});
export default React.memo(CategorySelector);

View file

@ -0,0 +1,94 @@
import React from 'react';
import { View, Text, TouchableOpacity, StyleSheet, Dimensions } from 'react-native';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { colors } from '../../styles';
import { StreamingContent } from '../../services/catalogService';
interface ContentItemProps {
item: StreamingContent;
onPress: () => void;
width?: number;
}
const ContentItem = ({ item, onPress, width }: ContentItemProps) => {
const { width: screenWidth } = Dimensions.get('window');
const itemWidth = width || (screenWidth - 48) / 2.2; // Default to 2 items per row with spacing
return (
<TouchableOpacity
style={[styles.container, { width: itemWidth }]}
onPress={onPress}
activeOpacity={0.6}
>
<View style={styles.posterContainer}>
<Image
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.poster}
contentFit="cover"
cachePolicy="memory-disk"
transition={300}
/>
<LinearGradient
colors={['transparent', 'rgba(0,0,0,0.85)']}
style={styles.gradient}
>
<Text style={styles.title} numberOfLines={2}>
{item.name}
</Text>
{item.year && (
<Text style={styles.year}>{item.year}</Text>
)}
</LinearGradient>
</View>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
container: {
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%',
},
gradient: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
padding: 16,
justifyContent: 'flex-end',
height: '45%',
},
title: {
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,
},
year: {
fontSize: 12,
color: 'rgba(255,255,255,0.7)',
textShadowColor: 'rgba(0, 0, 0, 0.75)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 2,
},
});
export default React.memo(ContentItem);

View file

@ -0,0 +1,94 @@
import React, { useCallback } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from 'react-native';
import { colors } from '../../styles';
interface GenreSelectorProps {
genres: string[];
selectedGenre: string;
onSelectGenre: (genre: string) => void;
}
const GenreSelector = ({
genres,
selectedGenre,
onSelectGenre
}: GenreSelectorProps) => {
const renderGenreButton = useCallback((genre: string) => {
const isSelected = selectedGenre === genre;
return (
<TouchableOpacity
key={genre}
style={[
styles.genreButton,
isSelected && styles.selectedGenreButton
]}
onPress={() => onSelectGenre(genre)}
activeOpacity={0.7}
>
<Text
style={[
styles.genreText,
isSelected && styles.selectedGenreText
]}
>
{genre}
</Text>
</TouchableOpacity>
);
}, [selectedGenre, onSelectGenre]);
return (
<View style={styles.container}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.scrollViewContent}
decelerationRate="fast"
snapToInterval={10}
>
{genres.map(renderGenreButton)}
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
container: {
paddingTop: 20,
paddingBottom: 12,
zIndex: 10,
},
scrollViewContent: {
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',
},
});
export default React.memo(GenreSelector);

42
src/constants/discover.ts Normal file
View file

@ -0,0 +1,42 @@
import { MaterialIcons } from '@expo/vector-icons';
import { StreamingContent } from '../services/catalogService';
export interface Category {
id: string;
name: string;
type: 'movie' | 'series' | 'channel' | 'tv';
icon: keyof typeof MaterialIcons.glyphMap;
}
export interface GenreCatalog {
genre: string;
items: StreamingContent[];
}
export 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
export const COMMON_GENRES = [
'All',
'Action',
'Adventure',
'Animation',
'Comedy',
'Crime',
'Documentary',
'Drama',
'Family',
'Fantasy',
'History',
'Horror',
'Music',
'Mystery',
'Romance',
'Science Fiction',
'Thriller',
'War',
'Western'
];

View file

@ -1,507 +1,41 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import React, { useState, useEffect, useCallback } 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 { catalogService, StreamingContent } from '../services/catalogService';
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;
}
// Components
import CategorySelector from '../components/discover/CategorySelector';
import GenreSelector from '../components/discover/GenreSelector';
import CatalogsList from '../components/discover/CatalogsList';
interface GenreCatalog {
genre: string;
items: StreamingContent[];
}
// Constants and types
import { CATEGORIES, COMMON_GENRES, Category, GenreCatalog } from '../constants/discover';
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 (
<TouchableOpacity
style={[
styles.categoryButton,
isSelected && styles.selectedCategoryButton
]}
onPress={onPress}
activeOpacity={0.7}
>
<MaterialIcons
name={category.icon}
size={24}
color={isSelected ? colors.white : colors.mediumGray}
/>
<Text
style={[
styles.categoryText,
isSelected && styles.selectedCategoryText
]}
>
{category.name}
</Text>
</TouchableOpacity>
);
});
const GenreButton = React.memo(({
genre,
isSelected,
onPress
}: {
genre: string;
isSelected: boolean;
onPress: () => void;
}) => {
const styles = useStyles();
return (
<TouchableOpacity
style={[
styles.genreButton,
isSelected && styles.selectedGenreButton
]}
onPress={onPress}
activeOpacity={0.7}
>
<Text
style={[
styles.genreText,
isSelected && styles.selectedGenreText
]}
>
{genre}
</Text>
</TouchableOpacity>
);
});
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 (
<TouchableOpacity
style={[styles.contentItem, { width: itemWidth }]}
onPress={onPress}
activeOpacity={0.6}
>
<View style={styles.posterContainer}>
<Image
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
style={styles.poster}
contentFit="cover"
cachePolicy="memory-disk"
transition={300}
/>
<LinearGradient
colors={['transparent', 'rgba(0,0,0,0.85)']}
style={styles.posterGradient}
>
<Text style={styles.contentTitle} numberOfLines={2}>
{item.name}
</Text>
{item.year && (
<Text style={styles.contentYear}>{item.year}</Text>
)}
</LinearGradient>
</View>
</TouchableOpacity>
);
});
const CatalogSection = React.memo(({
catalog,
selectedCategory,
navigation
}: {
catalog: GenreCatalog;
selectedCategory: Category;
navigation: NavigationProp<RootStackParamList>;
}) => {
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 }) => (
<ContentItem
item={item}
onPress={() => 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(() => <View style={{ width: 16 }} />, []);
return (
<View style={styles.catalogContainer}>
<View style={styles.catalogHeader}>
<View style={styles.catalogTitleContainer}>
<Text style={styles.catalogTitle}>{catalog.genre}</Text>
<View style={styles.catalogTitleBar} />
</View>
<TouchableOpacity
onPress={handleSeeMorePress}
style={styles.seeAllButton}
activeOpacity={0.6}
>
<Text style={styles.seeAllText}>See All</Text>
<MaterialIcons name="arrow-forward-ios" color={colors.primary} size={14} />
</TouchableOpacity>
</View>
<FlatList
data={displayItems}
renderItem={renderItem}
keyExtractor={keyExtractor}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 16 }}
snapToInterval={itemWidth + 16}
decelerationRate="fast"
snapToAlignment="start"
ItemSeparatorComponent={ItemSeparator}
initialNumToRender={3}
maxToRenderPerBatch={3}
windowSize={3}
removeClippedSubviews={true}
/>
</View>
);
});
// 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,
},
});
};
// Styles
import useDiscoverStyles from '../styles/screens/discoverStyles';
const DiscoverScreen = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const [selectedCategory, setSelectedCategory] = useState<Category>(CATEGORIES[0]);
const [selectedGenre, setSelectedGenre] = useState<string>('All');
const [catalogs, setCatalogs] = useState<GenreCatalog[]>([]);
const [allContent, setAllContent] = useState<StreamingContent[]>([]);
const [loading, setLoading] = useState(true);
const styles = useStyles();
const styles = useDiscoverStyles();
const insets = useSafeAreaInsets();
// Force consistent status bar settings
@ -539,8 +73,6 @@ const DiscoverScreen = () => {
content.push(...catalog.items);
});
setAllContent(content);
if (genre === 'All') {
// Group by genres when "All" is selected
const genreCatalogs: GenreCatalog[] = [];
@ -578,7 +110,6 @@ const DiscoverScreen = () => {
} catch (error) {
logger.error('Failed to load content:', error);
setCatalogs([]);
setAllContent([]);
} finally {
setLoading(false);
}
@ -601,29 +132,25 @@ const DiscoverScreen = () => {
navigation.navigate('Search');
}, [navigation]);
// Memoize rendering functions
const renderCatalogItem = useCallback(({ item }: { item: GenreCatalog }) => (
<CatalogSection
catalog={item}
selectedCategory={selectedCategory}
navigation={navigation}
/>
), [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;
const renderEmptyState = () => (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>
No content found for {selectedGenre !== 'All' ? selectedGenre : 'these filters'}
</Text>
</View>
);
return (
<View style={styles.container}>
{/* Fixed position header background to prevent shifts */}
{/* Fixed position header background */}
<View style={[styles.headerBackground, { height: headerHeight }]} />
<View style={{ flex: 1 }}>
{/* Header Section with proper top spacing */}
{/* Header Section */}
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
<View style={styles.headerContent}>
<Text style={styles.headerTitle}>Discover</Text>
@ -641,41 +168,21 @@ const DiscoverScreen = () => {
</View>
</View>
{/* Rest of the content */}
{/* Content Container */}
<View style={styles.contentContainer}>
{/* Categories Section */}
<View style={styles.categoryContainer}>
<View style={styles.categoriesContent}>
{CATEGORIES.map((category) => (
<CategoryButton
key={category.id}
category={category}
isSelected={selectedCategory.id === category.id}
onPress={() => handleCategoryPress(category)}
/>
))}
</View>
</View>
<CategorySelector
categories={CATEGORIES}
selectedCategory={selectedCategory}
onSelectCategory={handleCategoryPress}
/>
{/* Genres Section */}
<View style={styles.genreContainer}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.genresScrollView}
decelerationRate="fast"
snapToInterval={10}
>
{COMMON_GENRES.map(genre => (
<GenreButton
key={genre}
genre={genre}
isSelected={selectedGenre === genre}
onPress={() => handleGenrePress(genre)}
/>
))}
</ScrollView>
</View>
<GenreSelector
genres={COMMON_GENRES}
selectedGenre={selectedGenre}
onSelectGenre={handleGenrePress}
/>
{/* Content Section */}
{loading ? (
@ -683,24 +190,11 @@ const DiscoverScreen = () => {
<ActivityIndicator size="large" color={colors.primary} />
</View>
) : catalogs.length > 0 ? (
<FlatList
data={catalogs}
renderItem={renderCatalogItem}
keyExtractor={catalogKeyExtractor}
contentContainerStyle={styles.catalogsContainer}
showsVerticalScrollIndicator={false}
initialNumToRender={3}
maxToRenderPerBatch={3}
windowSize={5}
removeClippedSubviews={Platform.OS === 'android'}
<CatalogsList
catalogs={catalogs}
selectedCategory={selectedCategory}
/>
) : (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>
No content found for {selectedGenre !== 'All' ? selectedGenre : 'these filters'}
</Text>
</View>
)}
) : renderEmptyState()}
</View>
</View>
</View>

View file

@ -0,0 +1,67 @@
import { StyleSheet, Dimensions } from 'react-native';
import { colors } from '../index';
const useDiscoverStyles = () => {
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)',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingTop: 80,
},
emptyText: {
color: colors.mediumGray,
fontSize: 16,
textAlign: 'center',
paddingHorizontal: 32,
},
});
};
export default useDiscoverStyles;