import React, { useState, useEffect, useCallback, memo, useRef, useMemo } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, Dimensions, Alert, Platform, ScrollView, Modal, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import FastImage from '@d11/react-native-fast-image'; import { useTheme } from '../../contexts/ThemeContext'; import { useSettings } from '../../hooks/useSettings'; import { useTrailer } from '../../contexts/TrailerContext'; import { logger } from '../../utils/logger'; import TrailerService from '../../services/trailerService'; import TrailerModal from './TrailerModal'; import Animated, { useSharedValue, withTiming, withDelay, useAnimatedStyle } from 'react-native-reanimated'; // Enhanced responsive breakpoints for Trailers Section const BREAKPOINTS = { phone: 0, tablet: 768, largeTablet: 1024, tv: 1440, }; interface TrailerVideo { id: string; key: string; name: string; site: string; size: number; type: string; official: boolean; published_at: string; seasonNumber: number | null; displayName?: string; } interface TrailersSectionProps { tmdbId: number | null; type: 'movie' | 'tv'; contentId: string; contentTitle: string; } interface CategorizedTrailers { [key: string]: TrailerVideo[]; } const TrailersSection: React.FC = memo(({ tmdbId, type, contentId, contentTitle }) => { const { currentTheme } = useTheme(); const { settings } = useSettings(); const { pauseTrailer } = useTrailer(); const [trailers, setTrailers] = useState({}); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedTrailer, setSelectedTrailer] = useState(null); const [modalVisible, setModalVisible] = useState(false); const [selectedCategory, setSelectedCategory] = useState('Trailer'); const [dropdownVisible, setDropdownVisible] = useState(false); const [backendAvailable, setBackendAvailable] = useState(null); // Enhanced responsive sizing for tablets and TV screens const deviceWidth = Dimensions.get('window').width; const deviceHeight = Dimensions.get('window').height; // Determine device type based on width const getDeviceType = useCallback(() => { if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; return 'phone'; }, [deviceWidth]); const deviceType = getDeviceType(); const isTablet = deviceType === 'tablet'; const isLargeTablet = deviceType === 'largeTablet'; const isTV = deviceType === 'tv'; const isLargeScreen = isTablet || isLargeTablet || isTV; // Enhanced spacing and padding const horizontalPadding = useMemo(() => { switch (deviceType) { case 'tv': return 32; case 'largeTablet': return 28; case 'tablet': return 24; default: return 16; // phone } }, [deviceType]); // Enhanced trailer card sizing const trailerCardWidth = useMemo(() => { switch (deviceType) { case 'tv': return 240; case 'largeTablet': return 220; case 'tablet': return 200; default: return 170; // phone } }, [deviceType]); const trailerCardSpacing = useMemo(() => { switch (deviceType) { case 'tv': return 16; case 'largeTablet': return 14; case 'tablet': return 12; default: return 12; // phone } }, [deviceType]); // Smooth reveal animation after trailers are fetched const sectionOpacitySV = useSharedValue(0); const sectionTranslateYSV = useSharedValue(8); const hasAnimatedRef = useRef(false); const sectionAnimatedStyle = useAnimatedStyle(() => ({ opacity: sectionOpacitySV.value, transform: [{ translateY: sectionTranslateYSV.value }], })); // Reset animation state before a new fetch starts const resetSectionAnimation = useCallback(() => { hasAnimatedRef.current = false; sectionOpacitySV.value = 0; sectionTranslateYSV.value = 8; }, [sectionOpacitySV, sectionTranslateYSV]); // Trigger animation once, 500ms after trailers are available const triggerSectionAnimation = useCallback(() => { if (hasAnimatedRef.current) return; hasAnimatedRef.current = true; sectionOpacitySV.value = withDelay(500, withTiming(1, { duration: 400 })); sectionTranslateYSV.value = withDelay(500, withTiming(0, { duration: 400 })); }, [sectionOpacitySV, sectionTranslateYSV]); // Fetch trailers from TMDB useEffect(() => { if (!tmdbId) return; const initializeTrailers = async () => { resetSectionAnimation(); setBackendAvailable(true); // Assume available, let TrailerService handle errors await fetchTrailers(); }; const fetchTrailers = async () => { setLoading(true); setError(null); try { logger.info('TrailersSection', `Fetching trailers for TMDB ID: ${tmdbId}, type: ${type}`); // First check if the movie/TV show exists const basicEndpoint = type === 'movie' ? `https://api.themoviedb.org/3/movie/${tmdbId}?api_key=d131017ccc6e5462a81c9304d21476de` : `https://api.themoviedb.org/3/tv/${tmdbId}?api_key=d131017ccc6e5462a81c9304d21476de`; const basicResponse = await fetch(basicEndpoint); if (!basicResponse.ok) { if (basicResponse.status === 404) { // 404 on basic endpoint means TMDB ID doesn't exist - this is normal logger.info('TrailersSection', `TMDB ID ${tmdbId} not found in TMDB (404) - skipping trailers`); setTrailers({}); // Empty trailers - section won't render return; } logger.error('TrailersSection', `TMDB basic endpoint failed: ${basicResponse.status} ${basicResponse.statusText}`); setError(`Failed to verify content: ${basicResponse.status}`); return; } let allVideos: any[] = []; if (type === 'movie') { // For movies, just fetch the main videos endpoint const videosEndpoint = `https://api.themoviedb.org/3/movie/${tmdbId}/videos?api_key=d131017ccc6e5462a81c9304d21476de&language=en-US`; logger.info('TrailersSection', `Fetching movie videos from: ${videosEndpoint}`); const response = await fetch(videosEndpoint); if (!response.ok) { // 404 is normal - means no videos exist for this content if (response.status === 404) { logger.info('TrailersSection', `No videos found for movie TMDB ID ${tmdbId} (404 response)`); setTrailers({}); // Empty trailers - section won't render return; } logger.error('TrailersSection', `Videos endpoint failed: ${response.status} ${response.statusText}`); throw new Error(`Failed to fetch trailers: ${response.status}`); } const data = await response.json(); allVideos = data.results || []; logger.info('TrailersSection', `Received ${allVideos.length} videos for movie TMDB ID ${tmdbId}`); } else { // For TV shows, fetch both main TV videos and season-specific videos logger.info('TrailersSection', `Fetching TV show videos and season trailers for TMDB ID ${tmdbId}`); // Get TV show details to know how many seasons there are const tvDetailsResponse = await fetch(basicEndpoint); const tvDetails = await tvDetailsResponse.json(); const numberOfSeasons = tvDetails.number_of_seasons || 0; logger.info('TrailersSection', `TV show has ${numberOfSeasons} seasons`); // Fetch main TV show videos const tvVideosEndpoint = `https://api.themoviedb.org/3/tv/${tmdbId}/videos?api_key=d131017ccc6e5462a81c9304d21476de&language=en-US`; const tvResponse = await fetch(tvVideosEndpoint); if (tvResponse.ok) { const tvData = await tvResponse.json(); // Add season info to main TV videos const mainVideos = (tvData.results || []).map((video: any) => ({ ...video, seasonNumber: null as number | null, // null indicates main TV show videos displayName: video.name })); allVideos.push(...mainVideos); logger.info('TrailersSection', `Received ${mainVideos.length} main TV videos`); } // Fetch videos from each season (skip season 0 which is specials) const seasonPromises = []; for (let seasonNum = 1; seasonNum <= numberOfSeasons; seasonNum++) { seasonPromises.push( fetch(`https://api.themoviedb.org/3/tv/${tmdbId}/season/${seasonNum}/videos?api_key=d131017ccc6e5462a81c9304d21476de&language=en-US`) .then(res => res.json()) .then(data => ({ seasonNumber: seasonNum, videos: data.results || [] })) .catch(err => { logger.warn('TrailersSection', `Failed to fetch season ${seasonNum} videos:`, err); return { seasonNumber: seasonNum, videos: [] }; }) ); } const seasonResults = await Promise.all(seasonPromises); // Add season videos to the collection seasonResults.forEach(result => { if (result.videos.length > 0) { const seasonVideos = result.videos.map((video: any) => ({ ...video, seasonNumber: result.seasonNumber as number | null, displayName: `Season ${result.seasonNumber} - ${video.name}` })); allVideos.push(...seasonVideos); logger.info('TrailersSection', `Season ${result.seasonNumber}: ${result.videos.length} videos`); } }); const totalSeasonVideos = seasonResults.reduce((sum, result) => sum + result.videos.length, 0); logger.info('TrailersSection', `Total videos collected: ${allVideos.length} (main: ${allVideos.filter(v => v.seasonNumber === null).length}, seasons: ${totalSeasonVideos})`); } const categorized = categorizeTrailers(allVideos); const totalVideos = Object.values(categorized).reduce((sum, videos) => sum + videos.length, 0); if (totalVideos === 0) { logger.info('TrailersSection', `No videos found for TMDB ID ${tmdbId} - this is normal`); setTrailers({}); // No trailers available setSelectedCategory(''); // No category selected } else { logger.info('TrailersSection', `Categorized ${totalVideos} videos into ${Object.keys(categorized).length} categories`); setTrailers(categorized); // Trigger smooth reveal after 1.5s since we have content triggerSectionAnimation(); // Auto-select the first available category, preferring "Trailer" const availableCategories = Object.keys(categorized); const preferredCategory = availableCategories.includes('Trailer') ? 'Trailer' : availableCategories.includes('Teaser') ? 'Teaser' : availableCategories[0]; setSelectedCategory(preferredCategory); } } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Failed to load trailers'; setError(errorMessage); logger.error('TrailersSection', 'Error fetching trailers:', err); } finally { setLoading(false); } }; initializeTrailers(); }, [tmdbId, type]); // Categorize trailers by type const categorizeTrailers = (videos: any[]): CategorizedTrailers => { const categories: CategorizedTrailers = {}; videos.forEach(video => { if (video.site !== 'YouTube') return; // Only YouTube videos const category = video.type; if (!categories[category]) { categories[category] = []; } categories[category].push(video); }); // Sort within each category: season trailers first (newest seasons), then main series, official first, then by date Object.keys(categories).forEach(category => { categories[category].sort((a, b) => { // Season trailers come before main series trailers if (a.seasonNumber !== null && b.seasonNumber === null) return -1; if (a.seasonNumber === null && b.seasonNumber !== null) return 1; // If both have season numbers, sort by season number (newest seasons first) if (a.seasonNumber !== null && b.seasonNumber !== null) { if (a.seasonNumber !== b.seasonNumber) { return b.seasonNumber - a.seasonNumber; // Higher season numbers first } } // Official trailers come first within the same season/main series group if (a.official && !b.official) return -1; if (!a.official && b.official) return 1; // If both are official or both are not, sort by published date (newest first) return new Date(b.published_at).getTime() - new Date(a.published_at).getTime(); }); }); // Sort categories: "Trailer" category first, then categories with official trailers, then alphabetically const sortedCategories = Object.keys(categories).sort((a, b) => { // "Trailer" category always comes first if (a === 'Trailer') return -1; if (b === 'Trailer') return 1; const aHasOfficial = categories[a].some(trailer => trailer.official); const bHasOfficial = categories[b].some(trailer => trailer.official); // Categories with official trailers come first (after Trailer) if (aHasOfficial && !bHasOfficial) return -1; if (!aHasOfficial && bHasOfficial) return 1; // If both have or don't have official trailers, sort alphabetically return a.localeCompare(b); }); // Create new object with sorted categories const sortedCategoriesObj: CategorizedTrailers = {}; sortedCategories.forEach(category => { sortedCategoriesObj[category] = categories[category]; }); return sortedCategoriesObj; }; // Handle trailer selection const handleTrailerPress = (trailer: TrailerVideo) => { // Pause hero section trailer when modal opens try { pauseTrailer(); } catch (error) { logger.warn('TrailersSection', 'Error pausing hero trailer:', error); } setSelectedTrailer(trailer); setModalVisible(true); }; // Handle modal close const handleModalClose = () => { setModalVisible(false); setSelectedTrailer(null); // Note: Hero trailer will resume automatically when modal closes // The HeroSection component handles resuming based on scroll position }; // Handle category selection const handleCategorySelect = (category: string) => { setSelectedCategory(category); setDropdownVisible(false); }; // Toggle dropdown const toggleDropdown = () => { setDropdownVisible(!dropdownVisible); }; // Get thumbnail URL for YouTube video const getYouTubeThumbnail = (videoId: string, quality: 'default' | 'hq' | 'maxres' = 'hq') => { const qualities = { default: `https://img.youtube.com/vi/${videoId}/default.jpg`, hq: `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`, maxres: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg` }; return qualities[quality]; }; // Format trailer type for display const formatTrailerType = (type: string): string => { switch (type) { case 'Trailer': return 'Official Trailers'; case 'Teaser': return 'Teasers'; case 'Clip': return 'Clips & Scenes'; case 'Featurette': return 'Featurettes'; case 'Behind the Scenes': return 'Behind the Scenes'; default: return type; } }; // Get icon for trailer type const getTrailerTypeIcon = (type: string): string => { switch (type) { case 'Trailer': return 'movie'; case 'Teaser': return 'videocam'; case 'Clip': return 'content-cut'; case 'Featurette': return 'featured-video'; case 'Behind the Scenes': return 'camera'; default: return 'play-circle-outline'; } }; if (!tmdbId) { return null; // Don't show if no TMDB ID } // Don't render if backend availability is still being checked or backend is unavailable if (backendAvailable === null || backendAvailable === false) { return null; } // Don't render if TMDB enrichment is disabled if (!settings?.enrichMetadataWithTMDB) { return null; } if (loading) { return null; } if (error) { return null; } const trailerCategories = Object.keys(trailers); const totalVideos = Object.values(trailers).reduce((sum, videos) => sum + videos.length, 0); // Don't show section if no trailers (this is normal for many movies/TV shows) if (trailerCategories.length === 0 || totalVideos === 0) { // In development, show a subtle indicator that the section checked but found no trailers if (__DEV__) { return ( Trailers No trailers available ); } return null; } return ( {/* Enhanced Header with Category Selector */} Trailers & Videos {/* Category Selector - Right Aligned */} {trailerCategories.length > 0 && selectedCategory && ( {formatTrailerType(selectedCategory)} )} {/* Category Dropdown Modal */} setDropdownVisible(false)} > setDropdownVisible(false)} > {trailerCategories.map(category => ( handleCategorySelect(category)} activeOpacity={0.7} > {formatTrailerType(category)} {trailers[category].length} ))} {/* Selected Category Trailers */} {selectedCategory && trailers[selectedCategory] && ( {/* Trailers Horizontal Scroll */} {trailers[selectedCategory].map((trailer, index) => ( handleTrailerPress(trailer)} activeOpacity={0.9} > {/* Thumbnail with Gradient Overlay */} {/* Subtle Gradient Overlay */} {/* Trailer Info */} {trailer.displayName || trailer.name} {new Date(trailer.published_at).getFullYear()} ))} {/* Scroll Indicator - shows when there are more items to scroll */} {trailers[selectedCategory].length > (isTV ? 5 : isLargeTablet ? 4 : isTablet ? 4 : 3) && ( )} )} {/* Trailer Modal */} ); }); const styles = StyleSheet.create({ container: { marginTop: 24, marginBottom: 16, }, // Enhanced Header Styles header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', marginBottom: 0, gap: 12, }, headerTitle: { fontSize: 20, fontWeight: '700', letterSpacing: 0.5, }, // Category Selector Styles categorySelector: { flexDirection: 'row', alignItems: 'center', borderWidth: 1, borderRadius: 16, paddingHorizontal: 10, paddingVertical: 5, backgroundColor: 'rgba(255,255,255,0.03)', gap: 6, maxWidth: 160, // Limit maximum width to prevent overflow }, categorySelectorText: { fontSize: 12, fontWeight: '600', maxWidth: 120, // Limit text width }, // Dropdown Styles dropdownOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', alignItems: 'center', paddingHorizontal: 20, }, dropdownContainer: { width: '100%', maxWidth: 320, borderRadius: 16, borderWidth: 1, overflow: 'hidden', elevation: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8, }, dropdownItem: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, paddingVertical: 14, borderBottomWidth: 1, borderBottomColor: 'rgba(255,255,255,0.05)', }, dropdownItemContent: { flexDirection: 'row', alignItems: 'center', gap: 12, flex: 1, }, dropdownItemText: { fontSize: 16, flex: 1, }, dropdownItemCount: { fontSize: 12, opacity: 0.7, backgroundColor: 'rgba(255,255,255,0.1)', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 10, minWidth: 24, textAlign: 'center', }, // Selected Category Content selectedCategoryContent: { marginTop: 16, }, // Category Section Styles categorySection: { gap: 12, position: 'relative', // For scroll indicator positioning }, categoryHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, categoryTitleContainer: { flexDirection: 'row', alignItems: 'center', gap: 8, }, categoryIconContainer: { width: 28, height: 28, borderRadius: 8, alignItems: 'center', justifyContent: 'center', }, categoryTitle: { fontSize: 16, fontWeight: '600', }, categoryBadge: { borderRadius: 12, paddingHorizontal: 8, paddingVertical: 4, minWidth: 24, alignItems: 'center', }, categoryBadgeText: { fontSize: 12, fontWeight: '600', }, // Trailers Scroll View trailersScrollView: { marginHorizontal: -4, // Compensate for padding }, trailersScrollContent: { paddingHorizontal: 4, // Restore padding for first/last items paddingRight: 20, // Extra padding at end for scroll indicator }, // Enhanced Trailer Card Styles trailerCard: { backgroundColor: 'rgba(255,255,255,0.03)', borderRadius: 16, borderWidth: 1, borderColor: 'rgba(255,255,255,0.08)', overflow: 'hidden', elevation: 2, shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2, }, // Thumbnail Styles thumbnailWrapper: { position: 'relative', aspectRatio: 16 / 9, }, thumbnail: { width: '100%', height: '100%', borderTopLeftRadius: 16, borderTopRightRadius: 16, }, thumbnailGradient: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.2)', borderTopLeftRadius: 16, borderTopRightRadius: 16, }, // Trailer Info Styles trailerInfo: { padding: 12, }, trailerTitle: { fontSize: 12, fontWeight: '600', lineHeight: 16, marginBottom: 4, }, trailerMeta: { fontSize: 10, opacity: 0.7, fontWeight: '500', }, // Loading and Error States loadingContainer: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 32, gap: 12, }, loadingText: { fontSize: 14, opacity: 0.7, }, errorContainer: { alignItems: 'center', paddingVertical: 32, gap: 8, }, errorText: { fontSize: 14, textAlign: 'center', opacity: 0.7, }, // Scroll Indicator scrollIndicator: { position: 'absolute', right: 4, top: '50%', transform: [{ translateY: -10 }], width: 24, height: 20, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.3)', borderRadius: 12, }, // No Trailers State noTrailersContainer: { alignItems: 'center', paddingVertical: 24, }, noTrailersText: { fontSize: 14, opacity: 0.6, fontStyle: 'italic', }, }); export default TrailersSection;