NuvioStreaming_backup_24-10-25/src/components/home/ContentItem.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

206 lines
No EOL
6.2 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import { View, TouchableOpacity, ActivityIndicator, StyleSheet, Dimensions, Platform } from 'react-native';
import { Image as ExpoImage } from 'expo-image';
import { MaterialIcons } from '@expo/vector-icons';
import { useTheme } from '../../contexts/ThemeContext';
import { catalogService, StreamingContent } from '../../services/catalogService';
import { DropUpMenu } from './DropUpMenu';
interface ContentItemProps {
item: StreamingContent;
onPress: (id: string, type: string) => void;
}
const { width } = Dimensions.get('window');
// Dynamic poster calculation based on screen width - show 1/4 of next poster
const calculatePosterLayout = (screenWidth: number) => {
const MIN_POSTER_WIDTH = 100; // Reduced minimum for more posters
const MAX_POSTER_WIDTH = 130; // Reduced maximum for more posters
const LEFT_PADDING = 16; // Left padding
const SPACING = 8; // Space between posters
// Calculate available width for posters (reserve space for left padding)
const availableWidth = screenWidth - LEFT_PADDING;
// Try different numbers of full posters to find the best fit
let bestLayout = { numFullPosters: 3, posterWidth: 120 };
for (let n = 3; n <= 6; n++) {
// Calculate poster width needed for N full posters + 0.25 partial poster
// Formula: N * posterWidth + (N-1) * spacing + 0.25 * posterWidth = availableWidth - rightPadding
// Simplified: posterWidth * (N + 0.25) + (N-1) * spacing = availableWidth - rightPadding
// We'll use minimal right padding (8px) to maximize space
const usableWidth = availableWidth - 8;
const posterWidth = (usableWidth - (n - 1) * SPACING) / (n + 0.25);
if (posterWidth >= MIN_POSTER_WIDTH && posterWidth <= MAX_POSTER_WIDTH) {
bestLayout = { numFullPosters: n, posterWidth };
}
}
return {
numFullPosters: bestLayout.numFullPosters,
posterWidth: bestLayout.posterWidth,
spacing: SPACING,
partialPosterWidth: bestLayout.posterWidth * 0.25 // 1/4 of next poster
};
};
const posterLayout = calculatePosterLayout(width);
const POSTER_WIDTH = posterLayout.posterWidth;
const ContentItem = React.memo(({ item, onPress }: ContentItemProps) => {
const [menuVisible, setMenuVisible] = useState(false);
const [isWatched, setIsWatched] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
const [imageError, setImageError] = useState(false);
const { currentTheme } = useTheme();
const handleLongPress = useCallback(() => {
setMenuVisible(true);
}, []);
const handlePress = useCallback(() => {
onPress(item.id, item.type);
}, [item.id, item.type, onPress]);
const handleOptionSelect = useCallback((option: string) => {
switch (option) {
case 'library':
if (item.inLibrary) {
catalogService.removeFromLibrary(item.type, item.id);
} else {
catalogService.addToLibrary(item);
}
break;
case 'watched':
setIsWatched(prev => !prev);
break;
case 'playlist':
break;
case 'share':
break;
}
}, [item]);
const handleMenuClose = useCallback(() => {
setMenuVisible(false);
}, []);
return (
<>
<TouchableOpacity
style={styles.contentItem}
activeOpacity={0.7}
onPress={handlePress}
onLongPress={handleLongPress}
delayLongPress={300}
>
<View style={styles.contentItemContainer}>
<ExpoImage
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}
onLoadStart={() => {
setImageLoaded(false);
setImageError(false);
}}
onLoadEnd={() => setImageLoaded(true)}
onError={() => {
setImageError(true);
setImageLoaded(true);
}}
/>
{(!imageLoaded || imageError) && (
<View style={[styles.loadingOverlay, { backgroundColor: currentTheme.colors.elevation2 }]}>
{!imageError ? (
<ActivityIndicator color={currentTheme.colors.primary} size="small" />
) : (
<MaterialIcons name="broken-image" size={24} color={currentTheme.colors.lightGray} />
)}
</View>
)}
{isWatched && (
<View style={styles.watchedIndicator}>
<MaterialIcons name="check-circle" size={22} color={currentTheme.colors.success} />
</View>
)}
{item.inLibrary && (
<View style={styles.libraryBadge}>
<MaterialIcons name="bookmark" size={16} color={currentTheme.colors.white} />
</View>
)}
</View>
</TouchableOpacity>
<DropUpMenu
visible={menuVisible}
onClose={handleMenuClose}
item={item}
onOptionSelect={handleOptionSelect}
/>
</>
);
});
const styles = StyleSheet.create({
contentItem: {
width: POSTER_WIDTH,
aspectRatio: 2/3,
margin: 0,
borderRadius: 4,
overflow: 'hidden',
position: 'relative',
elevation: 6,
shadowColor: '#000',
shadowOffset: { width: 0, height: 3 },
shadowOpacity: 0.25,
shadowRadius: 6,
borderWidth: 0.5,
borderColor: 'rgba(255,255,255,0.12)',
},
contentItemContainer: {
width: '100%',
height: '100%',
borderRadius: 4,
overflow: 'hidden',
position: 'relative',
},
poster: {
width: '100%',
height: '100%',
borderRadius: 4,
},
loadingOverlay: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
justifyContent: 'center',
alignItems: 'center',
borderRadius: 8,
},
watchedIndicator: {
position: 'absolute',
top: 8,
right: 8,
borderRadius: 12,
padding: 2,
},
libraryBadge: {
position: 'absolute',
top: 8,
left: 8,
borderRadius: 8,
padding: 4,
},
});
export default ContentItem;