NuvioStreaming_backup_24-10-25/src/components/home/ThisWeekSection.tsx
tapframe 14dd507d50 Refactor image handling and caching strategies in multiple components for enhanced performance
This update modifies the image handling in ContentItem, ContinueWatchingSection, and FeaturedContent components to utilize a more efficient memory caching strategy and adjusted transition durations. Additionally, the HomeScreen component has been optimized for image prefetching, limiting concurrent requests to reduce memory pressure. The ThisWeekSection has been simplified to always refresh episodes when library items change, improving data handling. These changes aim to create a smoother user experience while navigating through content.
2025-06-21 23:48:38 +05:30

419 lines
No EOL
12 KiB
TypeScript

import React, { useEffect, useState, useCallback } from 'react';
import {
View,
Text,
StyleSheet,
FlatList,
TouchableOpacity,
ActivityIndicator,
Dimensions
} from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { NavigationProp } from '@react-navigation/native';
import { Image } from 'expo-image';
import { LinearGradient } from 'expo-linear-gradient';
import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../../contexts/ThemeContext';
import { stremioService } from '../../services/stremioService';
import { tmdbService } from '../../services/tmdbService';
import { useLibrary } from '../../hooks/useLibrary';
import { RootStackParamList } from '../../navigation/AppNavigator';
import { parseISO, isThisWeek, format, isAfter, isBefore } from 'date-fns';
import Animated, { FadeIn, FadeInRight } from 'react-native-reanimated';
const { width } = Dimensions.get('window');
const ITEM_WIDTH = width * 0.75; // Reduced width for better spacing
const ITEM_HEIGHT = 180; // Compact height for cleaner design
interface ThisWeekEpisode {
id: string;
seriesId: string;
seriesName: string;
title: string;
poster: string;
releaseDate: string;
season: number;
episode: number;
isReleased: boolean;
overview: string;
vote_average: number;
still_path: string | null;
season_poster_path: string | null;
}
export const ThisWeekSection = () => {
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const { libraryItems, loading: libraryLoading } = useLibrary();
const [episodes, setEpisodes] = useState<ThisWeekEpisode[]>([]);
const [loading, setLoading] = useState(true);
const { currentTheme } = useTheme();
const fetchThisWeekEpisodes = useCallback(async () => {
if (libraryItems.length === 0) {
setLoading(false);
return;
}
setLoading(true);
try {
const seriesItems = libraryItems.filter(item => item.type === 'series');
let allEpisodes: ThisWeekEpisode[] = [];
for (const series of seriesItems) {
try {
const metadata = await stremioService.getMetaDetails(series.type, series.id);
if (metadata?.videos) {
// Get TMDB ID for additional metadata
const tmdbId = await tmdbService.findTMDBIdByIMDB(series.id);
let tmdbEpisodes: { [key: string]: any } = {};
if (tmdbId) {
const allTMDBEpisodes = await tmdbService.getAllEpisodes(tmdbId);
// Flatten episodes into a map for easy lookup
Object.values(allTMDBEpisodes).forEach(seasonEpisodes => {
seasonEpisodes.forEach(episode => {
const key = `${episode.season_number}:${episode.episode_number}`;
tmdbEpisodes[key] = episode;
});
});
}
const thisWeekEpisodes = metadata.videos
.filter(video => {
if (!video.released) return false;
const releaseDate = parseISO(video.released);
return isThisWeek(releaseDate);
})
.map(video => {
const releaseDate = parseISO(video.released);
const tmdbEpisode = tmdbEpisodes[`${video.season}:${video.episode}`] || {};
return {
id: video.id,
seriesId: series.id,
seriesName: series.name || metadata.name,
title: tmdbEpisode.name || video.title || `Episode ${video.episode}`,
poster: series.poster || metadata.poster || '',
releaseDate: video.released,
season: video.season || 0,
episode: video.episode || 0,
isReleased: isBefore(releaseDate, new Date()),
overview: tmdbEpisode.overview || '',
vote_average: tmdbEpisode.vote_average || 0,
still_path: tmdbEpisode.still_path || null,
season_poster_path: tmdbEpisode.season_poster_path || null
};
});
allEpisodes = [...allEpisodes, ...thisWeekEpisodes];
}
} catch (error) {
console.error(`Error fetching episodes for ${series.name}:`, error);
}
}
// Sort episodes by release date
allEpisodes.sort((a, b) => {
return new Date(a.releaseDate).getTime() - new Date(b.releaseDate).getTime();
});
setEpisodes(allEpisodes);
} catch (error) {
console.error('Error fetching this week episodes:', error);
} finally {
setLoading(false);
}
}, [libraryItems]);
// Load episodes when library items change
useEffect(() => {
if (!libraryLoading) {
console.log('[ThisWeekSection] Library items changed, refreshing episodes. Items count:', libraryItems.length);
fetchThisWeekEpisodes();
}
}, [libraryLoading, libraryItems, fetchThisWeekEpisodes]);
const handleEpisodePress = (episode: ThisWeekEpisode) => {
// For upcoming episodes, go to the metadata screen
if (!episode.isReleased) {
const episodeId = `${episode.seriesId}:${episode.season}:${episode.episode}`;
navigation.navigate('Metadata', {
id: episode.seriesId,
type: 'series',
episodeId
});
return;
}
// For released episodes, go to the streams screen
const episodeId = `${episode.seriesId}:${episode.season}:${episode.episode}`;
navigation.navigate('Streams', {
id: episode.seriesId,
type: 'series',
episodeId
});
};
const handleViewAll = () => {
navigation.navigate('Calendar' as any);
};
if (loading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={currentTheme.colors.primary} />
<Text style={[styles.loadingText, { color: currentTheme.colors.textMuted }]}>
Loading this week's episodes...
</Text>
</View>
);
}
if (episodes.length === 0) {
return null;
}
const renderEpisodeItem = ({ item, index }: { item: ThisWeekEpisode, index: number }) => {
const releaseDate = parseISO(item.releaseDate);
const formattedDate = format(releaseDate, 'E, MMM d');
const isReleased = item.isReleased;
// Use episode still image if available, fallback to series poster
const imageUrl = item.still_path ?
tmdbService.getImageUrl(item.still_path) :
(item.season_poster_path ?
tmdbService.getImageUrl(item.season_poster_path) :
item.poster);
return (
<Animated.View
entering={FadeInRight.delay(index * 50).duration(300)}
style={styles.episodeItemContainer}
>
<TouchableOpacity
style={[
styles.episodeItem,
{
shadowColor: currentTheme.colors.black,
backgroundColor: currentTheme.colors.background,
}
]}
onPress={() => handleEpisodePress(item)}
activeOpacity={0.8}
>
<View style={styles.imageContainer}>
<Image
source={{ uri: imageUrl }}
style={styles.poster}
contentFit="cover"
transition={400}
/>
{/* Enhanced gradient overlay */}
<LinearGradient
colors={[
'transparent',
'transparent',
'rgba(0,0,0,0.4)',
'rgba(0,0,0,0.8)',
'rgba(0,0,0,0.95)'
]}
style={styles.gradient}
locations={[0, 0.4, 0.6, 0.8, 1]}
>
{/* Content area */}
<View style={styles.contentArea}>
<Text style={[styles.seriesName, { color: currentTheme.colors.white }]} numberOfLines={1}>
{item.seriesName}
</Text>
<Text style={[styles.episodeTitle, { color: 'rgba(255,255,255,0.9)' }]} numberOfLines={2}>
{item.title}
</Text>
{item.overview && (
<Text style={[styles.overview, { color: 'rgba(255,255,255,0.8)' }]} numberOfLines={2}>
{item.overview}
</Text>
)}
<View style={styles.dateContainer}>
<Text style={[styles.episodeInfo, { color: 'rgba(255,255,255,0.7)' }]}>
S{item.season}:E{item.episode} •
</Text>
<MaterialIcons
name="event"
size={14}
color={currentTheme.colors.primary}
/>
<Text style={[styles.releaseDate, { color: currentTheme.colors.primary }]}>
{formattedDate}
</Text>
</View>
</View>
</LinearGradient>
</View>
</TouchableOpacity>
</Animated.View>
);
};
return (
<Animated.View entering={FadeIn.duration(300)} style={styles.container}>
<View style={styles.header}>
<View style={styles.titleContainer}>
<Text style={[styles.title, { color: currentTheme.colors.text }]}>This Week</Text>
<View style={[styles.titleUnderline, { backgroundColor: currentTheme.colors.primary }]} />
</View>
<TouchableOpacity onPress={handleViewAll} style={styles.viewAllButton}>
<Text style={[styles.viewAllText, { color: currentTheme.colors.textMuted }]}>View All</Text>
<MaterialIcons name="chevron-right" size={20} color={currentTheme.colors.textMuted} />
</TouchableOpacity>
</View>
<FlatList
data={episodes}
keyExtractor={(item) => item.id}
renderItem={renderEpisodeItem}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.listContent}
snapToInterval={ITEM_WIDTH + 16}
decelerationRate="fast"
snapToAlignment="start"
ItemSeparatorComponent={() => <View style={{ width: 16 }} />}
/>
</Animated.View>
);
};
const styles = StyleSheet.create({
container: {
marginVertical: 20,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 16,
marginBottom: 16,
},
titleContainer: {
position: 'relative',
},
title: {
fontSize: 24,
fontWeight: '800',
letterSpacing: 0.5,
marginBottom: 4,
},
titleUnderline: {
position: 'absolute',
bottom: -2,
left: 0,
width: 40,
height: 3,
borderRadius: 2,
opacity: 0.8,
},
viewAllButton: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 8,
paddingHorizontal: 10,
borderRadius: 20,
backgroundColor: 'rgba(255,255,255,0.1)',
marginRight: -10,
},
viewAllText: {
fontSize: 14,
fontWeight: '600',
marginRight: 4,
},
listContent: {
paddingLeft: 16,
paddingRight: 16,
paddingBottom: 8,
},
loadingContainer: {
padding: 32,
alignItems: 'center',
},
loadingText: {
marginTop: 12,
fontSize: 16,
fontWeight: '500',
},
episodeItemContainer: {
width: ITEM_WIDTH,
height: ITEM_HEIGHT,
},
episodeItem: {
width: '100%',
height: '100%',
borderRadius: 16,
overflow: 'hidden',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.3,
shadowRadius: 12,
elevation: 12,
},
imageContainer: {
width: '100%',
height: '100%',
position: 'relative',
},
poster: {
width: '100%',
height: '100%',
borderRadius: 16,
},
gradient: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
justifyContent: 'flex-end',
padding: 12,
borderRadius: 16,
},
contentArea: {
width: '100%',
},
seriesName: {
fontSize: 16,
fontWeight: '700',
marginBottom: 6,
},
episodeTitle: {
fontSize: 14,
fontWeight: '600',
marginBottom: 4,
lineHeight: 18,
},
overview: {
fontSize: 12,
lineHeight: 16,
marginBottom: 6,
opacity: 0.9,
},
dateContainer: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 4,
},
episodeInfo: {
fontSize: 12,
fontWeight: '600',
marginRight: 4,
},
releaseDate: {
fontSize: 13,
fontWeight: '600',
marginLeft: 6,
letterSpacing: 0.3,
},
});