NuvioStreaming_backup_24-10-25/src/components/metadata/HeroSection.tsx
tapframe ddf8d007b7 Update progress thresholds across components to 85% for improved user experience
This update modifies various components, including ContinueWatchingSection, HeroSection, SeriesContent, and player components, to adjust the progress completion threshold from 95% to 85%. This change ensures that users can continue watching content that is closer to completion, enhancing engagement and usability. Additionally, related logic in the useWatchProgress hook and HomeScreen is updated to reflect this new threshold, providing a consistent experience across the application.
2025-06-20 18:57:41 +05:30

1231 lines
No EOL
35 KiB
TypeScript

import React, { useState, useEffect, useMemo } from 'react';
import {
View,
Text,
StyleSheet,
Dimensions,
TouchableOpacity,
Platform,
} from 'react-native';
import { MaterialIcons } from '@expo/vector-icons';
import { LinearGradient } from 'expo-linear-gradient';
import { Image } from 'expo-image';
import { BlurView as ExpoBlurView } from 'expo-blur';
import { BlurView as CommunityBlurView } from '@react-native-community/blur';
import Constants, { ExecutionEnvironment } from 'expo-constants';
import Animated, {
useAnimatedStyle,
interpolate,
Extrapolate,
useSharedValue,
withTiming,
runOnJS,
withRepeat,
} from 'react-native-reanimated';
import { useTheme } from '../../contexts/ThemeContext';
import { useTraktContext } from '../../contexts/TraktContext';
import { logger } from '../../utils/logger';
import { TMDBService } from '../../services/tmdbService';
const { width, height } = Dimensions.get('window');
// Ultra-optimized animation constants
const PARALLAX_FACTOR = 0.3;
const SCALE_FACTOR = 1.02;
const FADE_THRESHOLD = 200;
// Types - streamlined
interface HeroSectionProps {
metadata: any;
bannerImage: string | null;
loadingBanner: boolean;
logoLoadError: boolean;
scrollY: Animated.SharedValue<number>;
heroHeight: Animated.SharedValue<number>;
heroOpacity: Animated.SharedValue<number>;
logoOpacity: Animated.SharedValue<number>;
buttonsOpacity: Animated.SharedValue<number>;
buttonsTranslateY: Animated.SharedValue<number>;
watchProgressOpacity: Animated.SharedValue<number>;
watchProgressWidth: Animated.SharedValue<number>;
watchProgress: {
currentTime: number;
duration: number;
lastUpdated: number;
episodeId?: string;
traktSynced?: boolean;
traktProgress?: number;
} | null;
type: 'movie' | 'series';
getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null;
handleShowStreams: () => void;
handleToggleLibrary: () => void;
inLibrary: boolean;
id: string;
navigation: any;
getPlayButtonText: () => string;
setBannerImage: (bannerImage: string | null) => void;
setLogoLoadError: (error: boolean) => void;
}
// Ultra-optimized ActionButtons Component - minimal re-renders
const ActionButtons = React.memo(({
handleShowStreams,
toggleLibrary,
inLibrary,
type,
id,
navigation,
playButtonText,
animatedStyle,
isWatched,
watchProgress
}: {
handleShowStreams: () => void;
toggleLibrary: () => void;
inLibrary: boolean;
type: 'movie' | 'series';
id: string;
navigation: any;
playButtonText: string;
animatedStyle: any;
isWatched: boolean;
watchProgress: any;
}) => {
const { currentTheme } = useTheme();
// Memoized navigation handler
const handleRatingsPress = useMemo(() => async () => {
let finalTmdbId: number | null = null;
if (id?.startsWith('tmdb:')) {
const numericPart = id.split(':')[1];
const parsedId = parseInt(numericPart, 10);
if (!isNaN(parsedId)) {
finalTmdbId = parsedId;
}
} else if (id?.startsWith('tt')) {
try {
const tmdbService = TMDBService.getInstance();
const convertedId = await tmdbService.findTMDBIdByIMDB(id);
if (convertedId) {
finalTmdbId = convertedId;
}
} catch (error) {
logger.error(`[HeroSection] Error converting IMDb ID ${id}:`, error);
}
} else if (id) {
const parsedId = parseInt(id, 10);
if (!isNaN(parsedId)) {
finalTmdbId = parsedId;
}
}
if (finalTmdbId !== null) {
navigation.navigate('ShowRatings', { showId: finalTmdbId });
}
}, [id, navigation]);
// Determine play button style and text based on watched status
const playButtonStyle = useMemo(() => {
if (isWatched) {
return [styles.actionButton, styles.playButton, styles.watchedPlayButton];
}
return [styles.actionButton, styles.playButton];
}, [isWatched]);
const playButtonTextStyle = useMemo(() => {
if (isWatched) {
return [styles.playButtonText, styles.watchedPlayButtonText];
}
return styles.playButtonText;
}, [isWatched]);
const finalPlayButtonText = useMemo(() => {
if (isWatched) {
return 'Watch Again';
}
return playButtonText;
}, [isWatched, playButtonText]);
return (
<Animated.View style={[styles.actionButtons, animatedStyle]}>
<TouchableOpacity
style={playButtonStyle}
onPress={handleShowStreams}
activeOpacity={0.85}
>
<MaterialIcons
name={isWatched ? "replay" : (playButtonText === 'Resume' ? "play-circle-outline" : "play-arrow")}
size={24}
color={isWatched ? "#fff" : "#000"}
/>
<Text style={playButtonTextStyle}>{finalPlayButtonText}</Text>
{/* Subtle watched indicator in play button */}
{isWatched && (
<View style={styles.watchedIndicator}>
<MaterialIcons name="check" size={12} color="#fff" />
</View>
)}
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionButton, styles.infoButton]}
onPress={toggleLibrary}
activeOpacity={0.85}
>
{Platform.OS === 'ios' ? (
<ExpoBlurView intensity={80} style={styles.blurBackground} tint="dark" />
) : (
Constants.executionEnvironment === ExecutionEnvironment.StoreClient ? (
<View style={styles.androidFallbackBlur} />
) : (
<CommunityBlurView
style={styles.blurBackground}
blurType="dark"
blurAmount={8}
overlayColor="rgba(255,255,255,0.1)"
reducedTransparencyFallbackColor="rgba(255,255,255,0.15)"
/>
)
)}
<MaterialIcons
name={inLibrary ? 'bookmark' : 'bookmark-border'}
size={24}
color={currentTheme.colors.white}
/>
<Text style={styles.infoButtonText}>
{inLibrary ? 'Saved' : 'Save'}
</Text>
</TouchableOpacity>
{type === 'series' && (
<TouchableOpacity
style={styles.iconButton}
onPress={handleRatingsPress}
activeOpacity={0.85}
>
{Platform.OS === 'ios' ? (
<ExpoBlurView intensity={80} style={styles.blurBackgroundRound} tint="dark" />
) : (
Constants.executionEnvironment === ExecutionEnvironment.StoreClient ? (
<View style={styles.androidFallbackBlurRound} />
) : (
<CommunityBlurView
style={styles.blurBackgroundRound}
blurType="dark"
blurAmount={8}
overlayColor="rgba(255,255,255,0.1)"
reducedTransparencyFallbackColor="rgba(255,255,255,0.15)"
/>
)
)}
<MaterialIcons
name="assessment"
size={24}
color={currentTheme.colors.white}
/>
</TouchableOpacity>
)}
</Animated.View>
);
});
// Enhanced WatchProgress Component with Trakt integration and watched status
const WatchProgressDisplay = React.memo(({
watchProgress,
type,
getEpisodeDetails,
animatedStyle,
isWatched
}: {
watchProgress: {
currentTime: number;
duration: number;
lastUpdated: number;
episodeId?: string;
traktSynced?: boolean;
traktProgress?: number;
} | null;
type: 'movie' | 'series';
getEpisodeDetails: (episodeId: string) => { seasonNumber: string; episodeNumber: string; episodeName: string } | null;
animatedStyle: any;
isWatched: boolean;
}) => {
const { currentTheme } = useTheme();
const { isAuthenticated: isTraktAuthenticated, forceSyncTraktProgress } = useTraktContext();
// Animated values for enhanced effects
const completionGlow = useSharedValue(0);
const celebrationScale = useSharedValue(1);
const progressPulse = useSharedValue(1);
const progressBoxOpacity = useSharedValue(0);
const progressBoxScale = useSharedValue(0.8);
const progressBoxTranslateY = useSharedValue(20);
// Handle manual Trakt sync
const handleTraktSync = useMemo(() => async () => {
if (isTraktAuthenticated && forceSyncTraktProgress) {
logger.log('[HeroSection] Manual Trakt sync requested');
try {
const success = await forceSyncTraktProgress();
logger.log(`[HeroSection] Manual Trakt sync ${success ? 'successful' : 'failed'}`);
} catch (error) {
logger.error('[HeroSection] Manual Trakt sync error:', error);
}
}
}, [isTraktAuthenticated, forceSyncTraktProgress]);
// Memoized progress calculation with Trakt integration
const progressData = useMemo(() => {
// If content is fully watched, show watched status instead of progress
if (isWatched) {
let episodeInfo = '';
if (type === 'series' && watchProgress?.episodeId) {
const details = getEpisodeDetails(watchProgress.episodeId);
if (details) {
episodeInfo = ` • S${details.seasonNumber}:E${details.episodeNumber}${details.episodeName ? ` - ${details.episodeName}` : ''}`;
}
}
const watchedDate = watchProgress?.lastUpdated
? new Date(watchProgress.lastUpdated).toLocaleDateString()
: new Date().toLocaleDateString();
// Determine if watched via Trakt or local
const watchedViaTrakt = isTraktAuthenticated &&
watchProgress?.traktProgress !== undefined &&
watchProgress.traktProgress >= 95;
return {
progressPercent: 100,
formattedTime: watchedDate,
episodeInfo,
displayText: watchedViaTrakt ? 'Watched on Trakt' : 'Watched',
syncStatus: isTraktAuthenticated && watchProgress?.traktSynced ? '' : '', // Clean look for watched
isTraktSynced: watchProgress?.traktSynced && isTraktAuthenticated,
isWatched: true
};
}
if (!watchProgress || watchProgress.duration === 0) return null;
// Determine which progress to show - prioritize Trakt if available and authenticated
let progressPercent;
let isUsingTraktProgress = false;
if (isTraktAuthenticated && watchProgress.traktProgress !== undefined) {
progressPercent = watchProgress.traktProgress;
isUsingTraktProgress = true;
} else {
progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100;
}
const formattedTime = new Date(watchProgress.lastUpdated).toLocaleDateString();
let episodeInfo = '';
if (type === 'series' && watchProgress.episodeId) {
const details = getEpisodeDetails(watchProgress.episodeId);
if (details) {
episodeInfo = ` • S${details.seasonNumber}:E${details.episodeNumber}${details.episodeName ? ` - ${details.episodeName}` : ''}`;
}
}
// Enhanced display text with Trakt integration
let displayText = progressPercent >= 85 ? 'Watched' : `${Math.round(progressPercent)}% watched`;
let syncStatus = '';
// Show Trakt sync status if user is authenticated
if (isTraktAuthenticated) {
if (isUsingTraktProgress) {
syncStatus = ' • Using Trakt progress';
if (watchProgress.traktSynced) {
syncStatus = ' • Synced with Trakt';
}
} else if (watchProgress.traktSynced) {
syncStatus = ' • Synced with Trakt';
// If we have specific Trakt progress that differs from local, mention it
if (watchProgress.traktProgress !== undefined &&
Math.abs(progressPercent - watchProgress.traktProgress) > 5) {
displayText = `${Math.round(progressPercent)}% watched (${Math.round(watchProgress.traktProgress)}% on Trakt)`;
}
} else {
syncStatus = ' • Sync pending';
}
}
return {
progressPercent,
formattedTime,
episodeInfo,
displayText,
syncStatus,
isTraktSynced: watchProgress.traktSynced && isTraktAuthenticated,
isWatched: false
};
}, [watchProgress, type, getEpisodeDetails, isTraktAuthenticated, isWatched]);
// Trigger appearance and completion animations
useEffect(() => {
if (progressData) {
// Smooth entrance animation for the glassmorphic box
progressBoxOpacity.value = withTiming(1, { duration: 400 });
progressBoxScale.value = withTiming(1, { duration: 400 });
progressBoxTranslateY.value = withTiming(0, { duration: 400 });
if (progressData.isWatched || (progressData.progressPercent && progressData.progressPercent >= 85)) {
// Celebration animation sequence
celebrationScale.value = withRepeat(
withTiming(1.05, { duration: 200 }),
2,
true
);
// Glow effect
completionGlow.value = withRepeat(
withTiming(1, { duration: 1500 }),
-1,
true
);
} else {
// Subtle progress pulse for ongoing content
progressPulse.value = withRepeat(
withTiming(1.02, { duration: 2000 }),
-1,
true
);
}
} else {
// Hide animation when no progress data
progressBoxOpacity.value = withTiming(0, { duration: 300 });
progressBoxScale.value = withTiming(0.8, { duration: 300 });
progressBoxTranslateY.value = withTiming(20, { duration: 300 });
}
}, [progressData]);
// Animated styles for enhanced effects
const celebrationAnimatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: celebrationScale.value }],
}));
const glowAnimatedStyle = useAnimatedStyle(() => ({
opacity: interpolate(completionGlow.value, [0, 1], [0.3, 0.8], Extrapolate.CLAMP),
}));
const progressPulseStyle = useAnimatedStyle(() => ({
transform: [{ scale: progressPulse.value }],
}));
const progressBoxAnimatedStyle = useAnimatedStyle(() => ({
opacity: progressBoxOpacity.value,
transform: [
{ scale: progressBoxScale.value },
{ translateY: progressBoxTranslateY.value }
],
}));
if (!progressData) return null;
const isCompleted = progressData.isWatched || progressData.progressPercent >= 85;
return (
<Animated.View style={[styles.watchProgressContainer, animatedStyle]}>
{/* Glass morphism background with entrance animation */}
<Animated.View style={[styles.progressGlassBackground, progressBoxAnimatedStyle]}>
{Platform.OS === 'ios' ? (
<ExpoBlurView intensity={20} style={styles.blurBackground} tint="dark" />
) : (
<View style={styles.androidProgressBlur} />
)}
{/* Enhanced progress bar with glow effects */}
<Animated.View style={[styles.watchProgressBarContainer, celebrationAnimatedStyle]}>
<View style={styles.watchProgressBar}>
{/* Background glow for completed content */}
{isCompleted && (
<Animated.View style={[styles.completionGlow, glowAnimatedStyle]} />
)}
<Animated.View
style={[
styles.watchProgressFill,
!isCompleted && progressPulseStyle,
{
width: `${progressData.progressPercent}%`,
backgroundColor: isCompleted
? '#00ff88' // Bright green for completed
: progressData.isTraktSynced
? '#E50914' // Netflix red for Trakt synced content
: currentTheme.colors.primary,
// Add gradient effect for completed content
...(isCompleted && {
background: 'linear-gradient(90deg, #00ff88, #00cc6a)',
})
}
]}
/>
{/* Shimmer effect for active progress */}
{!isCompleted && progressData.progressPercent > 0 && (
<View style={styles.progressShimmer} />
)}
</View>
</Animated.View>
{/* Enhanced text container with better typography */}
<View style={styles.watchProgressTextContainer}>
<View style={styles.progressInfoMain}>
<Text style={[styles.watchProgressMainText, {
color: isCompleted ? '#00ff88' : currentTheme.colors.white,
fontSize: isCompleted ? 13 : 12,
fontWeight: isCompleted ? '700' : '600'
}]}>
{progressData.displayText}
</Text>
{/* Progress percentage badge */}
{!isCompleted && (
<View style={styles.percentageBadge}>
<Text style={styles.percentageText}>
{Math.round(progressData.progressPercent)}%
</Text>
</View>
)}
</View>
<Text style={[styles.watchProgressSubText, {
color: isCompleted ? 'rgba(0,255,136,0.7)' : currentTheme.colors.textMuted,
}]}>
{progressData.episodeInfo} Last watched {progressData.formattedTime}
</Text>
{/* Trakt sync status with enhanced styling */}
{progressData.syncStatus && (
<View style={styles.syncStatusContainer}>
<MaterialIcons
name={progressData.isTraktSynced ? "sync" : "sync-problem"}
size={12}
color={progressData.isTraktSynced ? "#E50914" : "rgba(255,255,255,0.6)"}
/>
<Text style={[styles.syncStatusText, {
color: progressData.isTraktSynced ? "#E50914" : "rgba(255,255,255,0.6)"
}]}>
{progressData.syncStatus}
</Text>
{/* Enhanced manual Trakt sync button - moved inline */}
{isTraktAuthenticated && forceSyncTraktProgress && (
<TouchableOpacity
style={styles.traktSyncButtonInline}
onPress={handleTraktSync}
activeOpacity={0.7}
>
<LinearGradient
colors={['#E50914', '#B8070F']}
style={styles.syncButtonGradientInline}
>
<MaterialIcons
name="refresh"
size={12}
color="#fff"
/>
</LinearGradient>
</TouchableOpacity>
)}
</View>
)}
</View>
</Animated.View>
</Animated.View>
);
});
const HeroSection: React.FC<HeroSectionProps> = ({
metadata,
bannerImage,
loadingBanner,
logoLoadError,
scrollY,
heroHeight,
heroOpacity,
logoOpacity,
buttonsOpacity,
buttonsTranslateY,
watchProgressOpacity,
watchProgress,
type,
getEpisodeDetails,
handleShowStreams,
handleToggleLibrary,
inLibrary,
id,
navigation,
getPlayButtonText,
setBannerImage,
setLogoLoadError,
}) => {
const { currentTheme } = useTheme();
const { isAuthenticated: isTraktAuthenticated } = useTraktContext();
// Enhanced state for smooth image loading
const [imageError, setImageError] = useState(false);
const [imageLoaded, setImageLoaded] = useState(false);
const imageOpacity = useSharedValue(1);
const imageLoadOpacity = useSharedValue(0);
const shimmerOpacity = useSharedValue(0.3);
// Memoized image source
const imageSource = useMemo(() =>
bannerImage || metadata.banner || metadata.poster
, [bannerImage, metadata.banner, metadata.poster]);
// Start shimmer animation for loading state
useEffect(() => {
if (!imageLoaded && imageSource) {
// Start shimmer animation
shimmerOpacity.value = withRepeat(
withTiming(0.8, { duration: 1200 }),
-1,
true
);
} else {
// Stop shimmer when loaded
shimmerOpacity.value = withTiming(0.3, { duration: 300 });
}
}, [imageLoaded, imageSource]);
// Reset loading state when image source changes
useEffect(() => {
if (imageSource) {
setImageLoaded(false);
imageLoadOpacity.value = 0;
}
}, [imageSource]);
// Enhanced image handlers with smooth transitions
const handleImageError = () => {
setImageError(true);
setImageLoaded(false);
imageOpacity.value = withTiming(0.6, { duration: 150 });
imageLoadOpacity.value = withTiming(0, { duration: 150 });
runOnJS(() => {
if (bannerImage !== metadata.banner) {
setBannerImage(metadata.banner || metadata.poster);
}
})();
};
const handleImageLoad = () => {
setImageError(false);
setImageLoaded(true);
imageOpacity.value = withTiming(1, { duration: 150 });
// Smooth fade-in for the loaded image
imageLoadOpacity.value = withTiming(1, { duration: 400 });
};
// Ultra-optimized animated styles - single calculations
const heroAnimatedStyle = useAnimatedStyle(() => ({
height: heroHeight.value,
opacity: heroOpacity.value,
}), []);
const logoAnimatedStyle = useAnimatedStyle(() => {
// Determine if progress bar should be shown
const hasProgress = watchProgress && watchProgress.duration > 0;
// Scale down logo when progress bar is present
const logoScale = hasProgress ? 0.85 : 1;
return {
opacity: logoOpacity.value,
transform: [
{
translateY: interpolate(
scrollY.value,
[0, 100],
[0, -20],
Extrapolate.CLAMP
)
},
{ scale: withTiming(logoScale, { duration: 300 }) }
]
};
}, [watchProgress]);
const watchProgressAnimatedStyle = useAnimatedStyle(() => ({
opacity: watchProgressOpacity.value,
}), []);
// Enhanced backdrop with smooth loading animation
const backdropImageStyle = useAnimatedStyle(() => {
'worklet';
const translateY = scrollY.value * PARALLAX_FACTOR;
const scale = 1 + (scrollY.value * 0.0001); // Micro scale effect
return {
opacity: imageOpacity.value * imageLoadOpacity.value,
transform: [
{ translateY: -Math.min(translateY, 100) }, // Cap translation
{ scale: Math.min(scale, SCALE_FACTOR) } // Cap scale
],
};
}, []);
// Simplified buttons animation
const buttonsAnimatedStyle = useAnimatedStyle(() => ({
opacity: buttonsOpacity.value,
transform: [{
translateY: interpolate(
buttonsTranslateY.value,
[0, 20],
[0, 20],
Extrapolate.CLAMP
)
}]
}), []);
// Ultra-optimized genre rendering
const genreElements = useMemo(() => {
if (!metadata?.genres?.length) return null;
const genresToDisplay = metadata.genres.slice(0, 3); // Reduced to 3 for performance
return genresToDisplay.map((genreName: string, index: number, array: string[]) => (
<React.Fragment key={`${genreName}-${index}`}>
<Text style={[styles.genreText, { color: currentTheme.colors.text }]}>
{genreName}
</Text>
{index < array.length - 1 && (
<Text style={[styles.genreDot, { color: currentTheme.colors.text }]}></Text>
)}
</React.Fragment>
));
}, [metadata.genres, currentTheme.colors.text]);
// Memoized play button text
const playButtonText = useMemo(() => getPlayButtonText(), [getPlayButtonText]);
// Calculate if content is watched (>=85% progress) - check both local and Trakt progress
const isWatched = useMemo(() => {
if (!watchProgress) return false;
// Check Trakt progress first if available and user is authenticated
if (isTraktAuthenticated && watchProgress.traktProgress !== undefined) {
const traktWatched = watchProgress.traktProgress >= 95;
logger.log(`[HeroSection] Trakt authenticated: ${isTraktAuthenticated}, Trakt progress: ${watchProgress.traktProgress}%, Watched: ${traktWatched}`);
return traktWatched;
}
// Fall back to local progress
if (watchProgress.duration === 0) return false;
const progressPercent = (watchProgress.currentTime / watchProgress.duration) * 100;
const localWatched = progressPercent >= 85;
logger.log(`[HeroSection] Local progress: ${progressPercent.toFixed(1)}%, Watched: ${localWatched}`);
return localWatched;
}, [watchProgress, isTraktAuthenticated]);
return (
<Animated.View style={[styles.heroSection, heroAnimatedStyle]}>
{/* Optimized Background */}
<View style={[styles.absoluteFill, { backgroundColor: currentTheme.colors.black }]} />
{/* Loading placeholder for smooth transition */}
{((imageSource && !imageLoaded) || loadingBanner) && (
<Animated.View style={[styles.absoluteFill, {
opacity: shimmerOpacity,
}]}>
<LinearGradient
colors={['#111', '#222', '#111']}
style={styles.absoluteFill}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
/>
</Animated.View>
)}
{/* Enhanced Background Image with smooth loading */}
{imageSource && !loadingBanner && (
<Animated.Image
source={{ uri: imageSource }}
style={[styles.absoluteFill, backdropImageStyle]}
resizeMode="cover"
onError={handleImageError}
onLoad={handleImageLoad}
/>
)}
{/* Simplified Gradient */}
<LinearGradient
colors={[
'rgba(0,0,0,0)',
'rgba(0,0,0,0.4)',
'rgba(0,0,0,0.8)',
currentTheme.colors.darkBackground
]}
locations={[0, 0.6, 0.85, 1]}
style={styles.heroGradient}
>
<View style={styles.heroContent}>
{/* Optimized Title/Logo */}
<View style={styles.logoContainer}>
<Animated.View style={[styles.titleLogoContainer, logoAnimatedStyle]}>
{metadata.logo && !logoLoadError ? (
<Image
source={{ uri: metadata.logo }}
style={styles.titleLogo}
contentFit="contain"
transition={150}
onError={() => {
runOnJS(setLogoLoadError)(true);
}}
/>
) : (
<Text style={[styles.heroTitle, { color: currentTheme.colors.highEmphasis }]}>
{metadata.name}
</Text>
)}
</Animated.View>
</View>
{/* Enhanced Watch Progress with Trakt integration */}
<WatchProgressDisplay
watchProgress={watchProgress}
type={type}
getEpisodeDetails={getEpisodeDetails}
animatedStyle={watchProgressAnimatedStyle}
isWatched={isWatched}
/>
{/* Optimized Genres */}
{genreElements && (
<View style={styles.genreContainer}>
{genreElements}
</View>
)}
{/* Optimized Action Buttons */}
<ActionButtons
handleShowStreams={handleShowStreams}
toggleLibrary={handleToggleLibrary}
inLibrary={inLibrary}
type={type}
id={id}
navigation={navigation}
playButtonText={playButtonText}
animatedStyle={buttonsAnimatedStyle}
isWatched={isWatched}
watchProgress={watchProgress}
/>
</View>
</LinearGradient>
</Animated.View>
);
};
// Ultra-optimized styles
const styles = StyleSheet.create({
heroSection: {
width: '100%',
backgroundColor: '#000',
overflow: 'hidden',
},
absoluteFill: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
},
heroGradient: {
flex: 1,
justifyContent: 'flex-end',
paddingBottom: 20,
},
heroContent: {
padding: 16,
paddingTop: 8,
paddingBottom: 8,
},
logoContainer: {
alignItems: 'center',
justifyContent: 'center',
width: '100%',
marginBottom: 4,
},
titleLogoContainer: {
alignItems: 'center',
justifyContent: 'center',
width: '100%',
},
titleLogo: {
width: width * 0.75,
height: 90,
alignSelf: 'center',
},
heroTitle: {
fontSize: 26,
fontWeight: '900',
marginBottom: 8,
textShadowColor: 'rgba(0,0,0,0.8)',
textShadowOffset: { width: 0, height: 1 },
textShadowRadius: 3,
letterSpacing: -0.3,
textAlign: 'center',
},
genreContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'center',
alignItems: 'center',
marginTop: 6,
marginBottom: 14,
gap: 6,
},
genreText: {
fontSize: 12,
fontWeight: '500',
opacity: 0.9,
},
genreDot: {
fontSize: 12,
fontWeight: '500',
opacity: 0.6,
marginHorizontal: 2,
},
actionButtons: {
flexDirection: 'row',
gap: 8,
alignItems: 'center',
justifyContent: 'center',
width: '100%',
position: 'relative',
},
actionButton: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 11,
paddingHorizontal: 16,
borderRadius: 26,
flex: 1,
},
playButton: {
backgroundColor: '#fff',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 4,
},
infoButton: {
borderWidth: 1.5,
borderColor: 'rgba(255,255,255,0.7)',
overflow: 'hidden',
},
iconButton: {
width: 50,
height: 50,
borderRadius: 25,
borderWidth: 1.5,
borderColor: 'rgba(255,255,255,0.7)',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
playButtonText: {
color: '#000',
fontWeight: '700',
marginLeft: 6,
fontSize: 15,
},
infoButtonText: {
color: '#fff',
marginLeft: 6,
fontWeight: '600',
fontSize: 15,
},
watchProgressContainer: {
marginTop: 4,
marginBottom: 4,
width: '100%',
alignItems: 'center',
minHeight: 36,
position: 'relative',
},
progressGlassBackground: {
width: '75%',
backgroundColor: 'rgba(255,255,255,0.08)',
borderRadius: 12,
padding: 8,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)',
overflow: 'hidden',
},
androidProgressBlur: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: 16,
backgroundColor: 'rgba(0,0,0,0.3)',
},
watchProgressBarContainer: {
position: 'relative',
marginBottom: 6,
},
watchProgressBar: {
width: '100%',
height: 3,
backgroundColor: 'rgba(255, 255, 255, 0.15)',
borderRadius: 1.5,
overflow: 'hidden',
position: 'relative',
},
watchProgressFill: {
height: '100%',
borderRadius: 1.25,
},
traktSyncIndicator: {
position: 'absolute',
right: 2,
top: -2,
bottom: -2,
width: 12,
alignItems: 'center',
justifyContent: 'center',
},
traktSyncIndicatorEnhanced: {
position: 'absolute',
right: 4,
top: -2,
bottom: -2,
width: 16,
height: 16,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
},
watchedProgressIndicator: {
position: 'absolute',
right: 2,
top: -1,
bottom: -1,
width: 10,
alignItems: 'center',
justifyContent: 'center',
},
watchProgressTextContainer: {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
},
watchProgressText: {
fontSize: 11,
textAlign: 'center',
opacity: 0.85,
letterSpacing: 0.1,
flex: 1,
},
traktSyncButton: {
padding: 4,
borderRadius: 12,
backgroundColor: 'rgba(255,255,255,0.1)',
},
blurBackground: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: 20,
},
androidFallbackBlur: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: 20,
backgroundColor: 'rgba(255,255,255,0.15)',
},
blurBackgroundRound: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: 25,
},
androidFallbackBlurRound: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: 25,
backgroundColor: 'rgba(255,255,255,0.15)',
},
watchedIndicator: {
position: 'absolute',
top: 4,
right: 4,
backgroundColor: 'rgba(0,0,0,0.6)',
borderRadius: 8,
width: 16,
height: 16,
alignItems: 'center',
justifyContent: 'center',
},
watchedPlayButton: {
backgroundColor: '#1e1e1e',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.3)',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 4,
},
watchedPlayButtonText: {
color: '#fff',
fontWeight: '700',
marginLeft: 6,
fontSize: 15,
},
// Enhanced progress indicator styles
progressShimmer: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
borderRadius: 2,
backgroundColor: 'rgba(255,255,255,0.1)',
},
completionGlow: {
position: 'absolute',
top: -2,
left: -2,
right: -2,
bottom: -2,
borderRadius: 4,
backgroundColor: 'rgba(0,255,136,0.2)',
},
completionIndicator: {
position: 'absolute',
right: 4,
top: -6,
bottom: -6,
width: 16,
height: 16,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
completionGradient: {
width: 16,
height: 16,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
sparkleContainer: {
position: 'absolute',
top: -10,
left: 0,
right: 0,
bottom: -10,
borderRadius: 2,
},
sparkle: {
position: 'absolute',
width: 8,
height: 8,
borderRadius: 4,
alignItems: 'center',
justifyContent: 'center',
},
progressInfoMain: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 2,
},
watchProgressMainText: {
fontSize: 11,
fontWeight: '600',
textAlign: 'center',
},
percentageBadge: {
backgroundColor: 'rgba(255,255,255,0.2)',
borderRadius: 8,
paddingHorizontal: 6,
paddingVertical: 2,
marginLeft: 8,
},
percentageText: {
fontSize: 10,
fontWeight: '600',
color: '#fff',
},
watchProgressSubText: {
fontSize: 9,
textAlign: 'center',
opacity: 0.8,
marginBottom: 1,
},
syncStatusContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
marginTop: 2,
width: '100%',
flexWrap: 'wrap',
},
syncStatusText: {
fontSize: 9,
marginLeft: 4,
fontWeight: '500',
},
traktSyncButtonEnhanced: {
position: 'absolute',
top: 8,
right: 8,
width: 24,
height: 24,
borderRadius: 12,
overflow: 'hidden',
},
traktSyncButtonInline: {
marginLeft: 8,
width: 20,
height: 20,
borderRadius: 10,
overflow: 'hidden',
},
syncButtonGradient: {
width: 24,
height: 24,
borderRadius: 12,
alignItems: 'center',
justifyContent: 'center',
},
syncButtonGradientInline: {
width: 20,
height: 20,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
},
traktIndicatorGradient: {
width: 16,
height: 16,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
});
export default React.memo(HeroSection);