mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
Remove Discover-related components and constants to streamline the application structure. This includes the deletion of CatalogSection, CatalogsList, CategorySelector, GenreSelector, ContentItem, and the DiscoverScreen, along with associated constants. The AppNavigator has been updated to reflect these changes, removing references to the Discover screen and adjusting navigation parameters accordingly.
This commit is contained in:
parent
929ed9cc9e
commit
e529ab388b
13 changed files with 24 additions and 800 deletions
|
|
@ -1,134 +0,0 @@
|
|||
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 { useTheme } from '../../contexts/ThemeContext';
|
||||
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 { currentTheme } = useTheme();
|
||||
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, { color: currentTheme.colors.white }]}>
|
||||
{catalog.genre}
|
||||
</Text>
|
||||
<View style={[styles.titleBar, { backgroundColor: currentTheme.colors.primary }]} />
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
onPress={handleSeeMorePress}
|
||||
style={[styles.seeAllButton, { backgroundColor: 'rgba(255,255,255,0.1)' }]}
|
||||
activeOpacity={0.6}
|
||||
>
|
||||
<Text style={[styles.seeAllText, { color: currentTheme.colors.textMuted }]}>View All</Text>
|
||||
<MaterialIcons name="chevron-right" size={20} color={currentTheme.colors.textMuted} />
|
||||
</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,
|
||||
marginTop: 6,
|
||||
borderRadius: 2,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: '700',
|
||||
},
|
||||
seeAllButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 10,
|
||||
borderRadius: 20,
|
||||
marginRight: -10,
|
||||
},
|
||||
seeAllText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
marginRight: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export default React.memo(CatalogSection);
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
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,
|
||||
paddingBottom: 90,
|
||||
},
|
||||
});
|
||||
|
||||
export default React.memo(CatalogsList);
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
import { Category } from '../../constants/discover';
|
||||
|
||||
interface CategorySelectorProps {
|
||||
categories: Category[];
|
||||
selectedCategory: Category;
|
||||
onSelectCategory: (category: Category) => void;
|
||||
}
|
||||
|
||||
const CategorySelector = ({
|
||||
categories,
|
||||
selectedCategory,
|
||||
onSelectCategory
|
||||
}: CategorySelectorProps) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
const renderCategoryButton = useCallback((category: Category) => {
|
||||
const isSelected = selectedCategory.id === category.id;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={category.id}
|
||||
style={[
|
||||
styles.categoryButton,
|
||||
isSelected && { backgroundColor: currentTheme.colors.primary }
|
||||
]}
|
||||
onPress={() => onSelectCategory(category)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={category.icon}
|
||||
size={24}
|
||||
color={isSelected ? currentTheme.colors.white : currentTheme.colors.mediumGray}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.categoryText,
|
||||
isSelected && { color: currentTheme.colors.white, fontWeight: '700' }
|
||||
]}
|
||||
>
|
||||
{category.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}, [selectedCategory, onSelectCategory, currentTheme]);
|
||||
|
||||
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: '#000',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 8,
|
||||
elevation: 4,
|
||||
},
|
||||
categoryText: {
|
||||
color: '#9e9e9e', // Default medium gray
|
||||
fontWeight: '600',
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
||||
export default React.memo(CategorySelector);
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
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 { useTheme } from '../../contexts/ThemeContext';
|
||||
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 { currentTheme } = useTheme();
|
||||
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, { shadowColor: currentTheme.colors.black }]}>
|
||||
<Image
|
||||
source={{ uri: item.poster || 'https://via.placeholder.com/300x450' }}
|
||||
style={styles.poster}
|
||||
contentFit="cover"
|
||||
cachePolicy="memory"
|
||||
transition={200}
|
||||
placeholder={{ uri: 'https://via.placeholder.com/300x450' }}
|
||||
placeholderContentFit="cover"
|
||||
recyclingKey={item.id}
|
||||
/>
|
||||
<LinearGradient
|
||||
colors={['transparent', 'rgba(0,0,0,0.85)']}
|
||||
style={styles.gradient}
|
||||
>
|
||||
<Text style={[styles.title, { color: currentTheme.colors.white }]} 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: 8,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: 'rgba(255,255,255,0.03)',
|
||||
elevation: 5,
|
||||
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',
|
||||
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);
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet, ScrollView } from 'react-native';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
|
||||
interface GenreSelectorProps {
|
||||
genres: string[];
|
||||
selectedGenre: string;
|
||||
onSelectGenre: (genre: string) => void;
|
||||
}
|
||||
|
||||
const GenreSelector = ({
|
||||
genres,
|
||||
selectedGenre,
|
||||
onSelectGenre
|
||||
}: GenreSelectorProps) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
const renderGenreButton = useCallback((genre: string) => {
|
||||
const isSelected = selectedGenre === genre;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={genre}
|
||||
style={[
|
||||
styles.genreButton,
|
||||
isSelected && { backgroundColor: currentTheme.colors.primary }
|
||||
]}
|
||||
onPress={() => onSelectGenre(genre)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.genreText,
|
||||
isSelected && { color: currentTheme.colors.white, fontWeight: '600' }
|
||||
]}
|
||||
>
|
||||
{genre}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}, [selectedGenre, onSelectGenre, currentTheme]);
|
||||
|
||||
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: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
genreText: {
|
||||
color: '#9e9e9e', // Default medium gray
|
||||
fontWeight: '500',
|
||||
fontSize: 14,
|
||||
},
|
||||
});
|
||||
|
||||
export default React.memo(GenreSelector);
|
||||
|
|
@ -176,4 +176,4 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
export default CatalogSection;
|
||||
export default React.memo(CatalogSection);
|
||||
|
|
@ -764,4 +764,4 @@ const styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
export default FeaturedContent;
|
||||
export default React.memo(FeaturedContent);
|
||||
|
|
@ -41,7 +41,7 @@ interface ThisWeekEpisode {
|
|||
season_poster_path: string | null;
|
||||
}
|
||||
|
||||
export const ThisWeekSection = () => {
|
||||
export const ThisWeekSection = React.memo(() => {
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const { libraryItems, loading: libraryLoading } = useLibrary();
|
||||
const [episodes, setEpisodes] = useState<ThisWeekEpisode[]>([]);
|
||||
|
|
@ -287,7 +287,7 @@ export const ThisWeekSection = () => {
|
|||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
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'
|
||||
];
|
||||
|
|
@ -17,7 +17,6 @@ import { useTheme } from '../contexts/ThemeContext';
|
|||
|
||||
// Import screens with their proper types
|
||||
import HomeScreen from '../screens/HomeScreen';
|
||||
import DiscoverScreen from '../screens/DiscoverScreen';
|
||||
import LibraryScreen from '../screens/LibraryScreen';
|
||||
import SettingsScreen from '../screens/SettingsScreen';
|
||||
import MetadataScreen from '../screens/MetadataScreen';
|
||||
|
|
@ -46,7 +45,6 @@ export type RootStackParamList = {
|
|||
Onboarding: undefined;
|
||||
MainTabs: undefined;
|
||||
Home: undefined;
|
||||
Discover: undefined;
|
||||
Library: undefined;
|
||||
Settings: undefined;
|
||||
Search: undefined;
|
||||
|
|
@ -110,8 +108,8 @@ export type RootStackNavigationProp = NativeStackNavigationProp<RootStackParamLi
|
|||
// Tab navigator types
|
||||
export type MainTabParamList = {
|
||||
Home: undefined;
|
||||
Discover: undefined;
|
||||
Library: undefined;
|
||||
Search: undefined;
|
||||
Settings: undefined;
|
||||
};
|
||||
|
||||
|
|
@ -298,7 +296,7 @@ export const CustomNavigationDarkTheme: Theme = {
|
|||
type IconNameType = 'home' | 'home-outline' | 'compass' | 'compass-outline' |
|
||||
'play-box-multiple' | 'play-box-multiple-outline' |
|
||||
'puzzle' | 'puzzle-outline' |
|
||||
'cog' | 'cog-outline';
|
||||
'cog' | 'cog-outline' | 'feature-search' | 'feature-search-outline';
|
||||
|
||||
// Add TabIcon component
|
||||
const TabIcon = React.memo(({ focused, color, iconName }: {
|
||||
|
|
@ -479,12 +477,12 @@ const MainTabs = () => {
|
|||
case 'Home':
|
||||
iconName = 'home';
|
||||
break;
|
||||
case 'Discover':
|
||||
iconName = 'compass';
|
||||
break;
|
||||
case 'Library':
|
||||
iconName = 'play-box-multiple';
|
||||
break;
|
||||
case 'Search':
|
||||
iconName = 'feature-search';
|
||||
break;
|
||||
case 'Settings':
|
||||
iconName = 'cog';
|
||||
break;
|
||||
|
|
@ -546,12 +544,12 @@ const MainTabs = () => {
|
|||
case 'Home':
|
||||
iconName = 'home';
|
||||
break;
|
||||
case 'Discover':
|
||||
iconName = 'compass';
|
||||
break;
|
||||
case 'Library':
|
||||
iconName = 'play-box-multiple';
|
||||
break;
|
||||
case 'Search':
|
||||
iconName = 'feature-search';
|
||||
break;
|
||||
case 'Settings':
|
||||
iconName = 'cog';
|
||||
break;
|
||||
|
|
@ -634,14 +632,6 @@ const MainTabs = () => {
|
|||
tabBarLabel: 'Home',
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Discover"
|
||||
component={DiscoverScreen}
|
||||
options={{
|
||||
tabBarLabel: 'Discover',
|
||||
headerShown: false
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Library"
|
||||
component={LibraryScreen}
|
||||
|
|
@ -650,6 +640,14 @@ const MainTabs = () => {
|
|||
headerShown: false
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Search"
|
||||
component={SearchScreen}
|
||||
options={{
|
||||
tabBarLabel: 'Search',
|
||||
headerShown: false
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Settings"
|
||||
component={SettingsScreen}
|
||||
|
|
|
|||
|
|
@ -1,205 +0,0 @@
|
|||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
ActivityIndicator,
|
||||
StatusBar,
|
||||
Dimensions,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
import { catalogService, StreamingContent } from '../services/catalogService';
|
||||
import { RootStackParamList } from '../navigation/AppNavigator';
|
||||
import { logger } from '../utils/logger';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
// Components
|
||||
import CategorySelector from '../components/discover/CategorySelector';
|
||||
import GenreSelector from '../components/discover/GenreSelector';
|
||||
import CatalogsList from '../components/discover/CatalogsList';
|
||||
|
||||
// Constants and types
|
||||
import { CATEGORIES, COMMON_GENRES, Category, GenreCatalog } from '../constants/discover';
|
||||
|
||||
// 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 [loading, setLoading] = useState(true);
|
||||
const styles = useDiscoverStyles();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
if (genre === 'All') {
|
||||
// Group by genres when "All" is selected
|
||||
const genreCatalogs: GenreCatalog[] = [];
|
||||
|
||||
// Get all genres from content
|
||||
const genresSet = new Set<string>();
|
||||
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([]);
|
||||
} 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]);
|
||||
|
||||
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 */}
|
||||
<View style={[styles.headerBackground, { height: headerHeight }]} />
|
||||
|
||||
<View style={{ flex: 1 }}>
|
||||
{/* Header Section */}
|
||||
<View style={[styles.header, { height: headerHeight, paddingTop: topSpacing }]}>
|
||||
<View style={styles.headerContent}>
|
||||
<Text style={styles.headerTitle}>Discover</Text>
|
||||
<TouchableOpacity
|
||||
onPress={handleSearchPress}
|
||||
style={styles.searchButton}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<MaterialIcons
|
||||
name="search"
|
||||
size={24}
|
||||
color={currentTheme.colors.white}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Content Container */}
|
||||
<View style={styles.contentContainer}>
|
||||
{/* Categories Section */}
|
||||
<CategorySelector
|
||||
categories={CATEGORIES}
|
||||
selectedCategory={selectedCategory}
|
||||
onSelectCategory={handleCategoryPress}
|
||||
/>
|
||||
|
||||
{/* Genres Section */}
|
||||
<GenreSelector
|
||||
genres={COMMON_GENRES}
|
||||
selectedGenre={selectedGenre}
|
||||
onSelectGenre={handleGenrePress}
|
||||
/>
|
||||
|
||||
{/* Content Section */}
|
||||
{loading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
|
||||
</View>
|
||||
) : catalogs.length > 0 ? (
|
||||
<CatalogsList
|
||||
catalogs={catalogs}
|
||||
selectedCategory={selectedCategory}
|
||||
/>
|
||||
) : renderEmptyState()}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(DiscoverScreen);
|
||||
|
|
@ -602,11 +602,10 @@ const HomeScreen = () => {
|
|||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
initialNumToRender={3}
|
||||
maxToRenderPerBatch={2}
|
||||
windowSize={5}
|
||||
removeClippedSubviews={Platform.OS === 'android'}
|
||||
updateCellsBatchingPeriod={50}
|
||||
initialNumToRender={5}
|
||||
maxToRenderPerBatch={5}
|
||||
windowSize={11}
|
||||
removeClippedSubviews={false}
|
||||
onEndReachedThreshold={0.5}
|
||||
maintainVisibleContentPosition={{
|
||||
minIndexForVisible: 0,
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
import { StyleSheet, Dimensions } from 'react-native';
|
||||
import { useTheme } from '../../contexts/ThemeContext';
|
||||
|
||||
const useDiscoverStyles = () => {
|
||||
const { width } = Dimensions.get('window');
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
return StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
},
|
||||
headerBackground: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: currentTheme.colors.darkBackground,
|
||||
zIndex: 1,
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: currentTheme.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: currentTheme.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,
|
||||
paddingBottom: 90,
|
||||
},
|
||||
emptyText: {
|
||||
color: currentTheme.colors.mediumGray,
|
||||
fontSize: 16,
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 32,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default useDiscoverStyles;
|
||||
Loading…
Reference in a new issue