import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { View, Text, TouchableOpacity, ScrollView, ActivityIndicator, Dimensions, Platform, FlatList, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import FastImage from '@d11/react-native-fast-image'; import Animated, { FadeIn, FadeOut, SlideInDown, SlideOutDown, useAnimatedStyle, useSharedValue, withTiming, withSpring, interpolate, Extrapolate, } from 'react-native-reanimated'; import { LinearGradient } from 'expo-linear-gradient'; import { BlurView } from 'expo-blur'; import { useTheme } from '../contexts/ThemeContext'; import { Cast } from '../types/cast'; import { tmdbService } from '../services/tmdbService'; import { catalogService } from '../services/catalogService'; import { useNavigation, useRoute, RouteProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { RootStackParamList } from '../navigation/AppNavigator'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { StackActions } from '@react-navigation/native'; import CustomAlert from '../components/CustomAlert'; const { width, height } = Dimensions.get('window'); const isTablet = width >= 768; const numColumns = isTablet ? 4 : 3; const HORIZONTAL_PADDING = 20; const posterWidth = (width - (HORIZONTAL_PADDING * 2) - (numColumns - 1) * 12) / numColumns; const posterHeight = posterWidth * 1.5; interface CastMovie { id: number; title: string; poster_path: string | null; release_date: string; character?: string; job?: string; media_type: 'movie' | 'tv'; popularity?: number; vote_average?: number; isUpcoming?: boolean; } type CastMoviesScreenRouteProp = RouteProp; const CastMoviesScreen: React.FC = () => { const { currentTheme } = useTheme(); const { t } = useTranslation(); const navigation = useNavigation>(); const route = useRoute(); const { castMember } = route.params; const { top: safeAreaTop } = useSafeAreaInsets(); const [movies, setMovies] = useState([]); const [loading, setLoading] = useState(true); const [selectedFilter, setSelectedFilter] = useState<'all' | 'movies' | 'tv'>('all'); const [sortBy, setSortBy] = useState<'popularity' | 'latest' | 'upcoming'>('popularity'); const scrollY = useSharedValue(0); const [displayLimit, setDisplayLimit] = useState(30); // Start with fewer items for performance const [isLoadingMore, setIsLoadingMore] = useState(false); const [alertVisible, setAlertVisible] = useState(false); const [alertTitle, setAlertTitle] = useState(''); const [alertMessage, setAlertMessage] = useState(''); const [alertActions, setAlertActions] = useState([]); useEffect(() => { if (castMember) { fetchCastCredits(); } }, [castMember]); // Reset display limit when filters change useEffect(() => { setDisplayLimit(30); }, [selectedFilter, sortBy]); const fetchCastCredits = async () => { if (!castMember) return; setLoading(true); try { const credits = await tmdbService.getPersonCombinedCredits(castMember.id); if (credits && credits.cast) { const currentDate = new Date(); // Combine cast roles with enhanced data, excluding talk shows and variety shows const allCredits = credits.cast .filter((item: any) => { // Filter out talk shows, variety shows, and ensure we have required data const hasPoster = item.poster_path; const hasReleaseDate = item.release_date || item.first_air_date; if (!hasPoster || !hasReleaseDate) return false; // Enhanced talk show filtering const title = (item.title || item.name || '').toLowerCase(); const overview = (item.overview || '').toLowerCase(); // List of common talk show and variety show keywords const talkShowKeywords = [ 'talk', 'show', 'late night', 'tonight show', 'jimmy fallon', 'snl', 'saturday night live', 'variety', 'sketch comedy', 'stand-up', 'standup', 'comedy central', 'daily show', 'colbert', 'kimmel', 'conan', 'ellen', 'oprah', 'view', 'today show', 'good morning', 'interview', 'panel', 'roundtable', 'discussion', 'news', 'current events', 'politics', 'reality', 'competition', 'game show', 'quiz', 'trivia', 'awards', 'ceremony', 'red carpet', 'premiere', 'after party', 'behind the scenes', 'making of', 'documentary', 'special', 'concert', 'live performance', 'mtv', 'vh1', 'bet', 'comedy', 'roast' ]; // Check if any keyword matches const isTalkShow = talkShowKeywords.some(keyword => title.includes(keyword) || overview.includes(keyword) ); return !isTalkShow; }) .map((item: any) => { const releaseDate = new Date(item.release_date || item.first_air_date); const isUpcoming = releaseDate > currentDate; return { id: item.id, title: item.title || item.name, poster_path: item.poster_path, release_date: item.release_date || item.first_air_date, character: item.character, media_type: item.media_type, popularity: item.popularity || 0, vote_average: item.vote_average || 0, isUpcoming, }; }); setMovies(allCredits); } } catch (error) { if (__DEV__) console.error('Error fetching cast credits:', error); } finally { setLoading(false); } }; const filteredAndSortedMovies = useMemo(() => { let filtered = movies.filter(movie => { if (selectedFilter === 'all') return true; if (selectedFilter === 'movies') return movie.media_type === 'movie'; if (selectedFilter === 'tv') return movie.media_type === 'tv'; return true; }); // If sorting by upcoming, only show upcoming content if (sortBy === 'upcoming') { filtered = filtered.filter(movie => movie.isUpcoming); } // Apply sorting filtered.sort((a, b) => { switch (sortBy) { case 'popularity': return (b.popularity || 0) - (a.popularity || 0); case 'latest': const dateA = new Date(a.release_date || '1900-01-01'); const dateB = new Date(b.release_date || '1900-01-01'); return dateB.getTime() - dateA.getTime(); case 'upcoming': // Only show upcoming content, sorted by nearest release date if (!a.isUpcoming && !b.isUpcoming) return 0; if (a.isUpcoming && !b.isUpcoming) return -1; if (!a.isUpcoming && b.isUpcoming) return 1; const upcomingDateA = new Date(a.release_date || '9999-12-31'); const upcomingDateB = new Date(b.release_date || '9999-12-31'); return upcomingDateA.getTime() - upcomingDateB.getTime(); default: return 0; } }); return filtered; }, [movies, selectedFilter, sortBy]); // Performance: Limit displayed items initially for better performance const displayedMovies = useMemo(() => { return filteredAndSortedMovies.slice(0, displayLimit); }, [filteredAndSortedMovies, displayLimit]); // Load more items when needed const handleLoadMore = useCallback(() => { if (displayLimit < filteredAndSortedMovies.length && !isLoadingMore) { setIsLoadingMore(true); // Simulate loading delay for smooth UX setTimeout(() => { setDisplayLimit(prev => Math.min(prev + 20, filteredAndSortedMovies.length)); setIsLoadingMore(false); }, 200); } }, [displayLimit, filteredAndSortedMovies.length, isLoadingMore]); const handleMoviePress = async (movie: CastMovie) => { if (__DEV__) { console.log('=== CastMoviesScreen: Movie Press ==='); console.log('Movie data:', { id: movie.id, title: movie.title, media_type: movie.media_type, release_date: movie.release_date, character: movie.character, popularity: movie.popularity, vote_average: movie.vote_average, isUpcoming: movie.isUpcoming }); } try { if (__DEV__) console.log('Attempting to get Stremio ID for:', movie.media_type, movie.id.toString()); // Get Stremio ID using catalogService const stremioId = await catalogService.getStremioId(movie.media_type, movie.id.toString()); if (__DEV__) console.log('Stremio ID result:', stremioId); if (stremioId) { if (__DEV__) console.log('Successfully found Stremio ID, navigating to Metadata with:', { id: stremioId, type: movie.media_type }); // Convert TMDB media type to Stremio media type const stremioType = movie.media_type === 'tv' ? 'series' : movie.media_type; if (__DEV__) console.log('Navigating with Stremio type conversion:', { originalType: movie.media_type, stremioType: stremioType, id: stremioId }); navigation.dispatch( StackActions.push('Metadata', { id: stremioId, type: stremioType }) ); } else { if (__DEV__) console.warn('Stremio ID is null/undefined for movie:', movie.title); throw new Error('Could not find Stremio ID'); } } catch (error: any) { if (__DEV__) { console.error('=== Error in handleMoviePress ==='); console.error('Movie:', movie.title); console.error('Error details:', error); console.error('Error message:', error.message); console.error('Error stack:', error.stack); } setAlertTitle(t('cast.alert_error_title')); setAlertMessage(t('cast.alert_error_message', { title: movie.title })); setAlertActions([{ label: t('cast.alert_ok'), onPress: () => { } }]); setAlertVisible(true); } }; const handleBack = () => { navigation.goBack(); }; const renderFilterButton = (filter: 'all' | 'movies' | 'tv', label: string, count: number) => { const isSelected = selectedFilter === filter; return ( setSelectedFilter(filter)} activeOpacity={0.8} > {count > 0 ? `${label} (${count})` : label} ); }; const renderSortButton = (sort: 'popularity' | 'latest' | 'upcoming', label: string, icon: string) => { const isSelected = sortBy === sort; return ( setSortBy(sort)} activeOpacity={0.7} > {label} ); }; const renderMovieItem = useCallback(({ item, index }: { item: CastMovie; index: number }) => ( handleMoviePress(item)} activeOpacity={0.85} style={{ shadowColor: '#000', shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.3, shadowRadius: 8, elevation: 8, }} > {item.poster_path ? ( ) : ( )} {/* Upcoming indicator */} {item.isUpcoming && ( {t('cast.upcoming_badge')} )} {/* Rating badge */} {item.vote_average && item.vote_average > 0 && ( {`${item.vote_average.toFixed(1)}`} )} {/* Gradient overlay for better text readability */} {`${item.title}`} {item.character && ( {t('cast.as_character', { character: item.character })} )} {item.release_date && ( {`${new Date(item.release_date).getFullYear()}`} )} {item.isUpcoming && ( {t('cast.coming_soon')} )} ), [posterWidth, posterHeight, handleMoviePress]); const movieCount = movies.filter(m => m.media_type === 'movie').length; const tvCount = movies.filter(m => m.media_type === 'tv').length; const upcomingCount = movies.filter(m => m.isUpcoming).length; // Animated header style const headerAnimatedStyle = useAnimatedStyle(() => { const opacity = interpolate( scrollY.value, [0, 100], [1, 0.9], Extrapolate.CLAMP ); return { opacity, }; }); return ( {/* Minimal Header */} {castMember?.profile_path ? ( ) : ( {castMember?.name ? castMember.name.split(' ').reduce((prev: string, current: string) => prev + current[0], '').substring(0, 2) : 'NA'} )} {`${castMember?.name}`} {t('cast.filmography_count', { count: movies.length })} {/* Filters and Sort */} {/* Filter Section */} {t('cast.filter')} {renderFilterButton('all', t('catalog.all'), movies.length)} {renderFilterButton('movies', t('catalog.movies'), movieCount)} {renderFilterButton('tv', t('catalog.tv_shows'), tvCount)} {/* Sort Section */} {t('cast.sort_by')} {renderSortButton('popularity', t('cast.sort_popular'), 'trending-up')} {renderSortButton('latest', t('cast.sort_latest'), 'schedule')} {renderSortButton('upcoming', t('cast.sort_upcoming'), 'event')} {/* Content */} {loading ? ( {t('cast.loading_filmography')} ) : ( `${item.media_type}-${item.id}`} numColumns={numColumns} contentContainerStyle={{ paddingHorizontal: 20, paddingTop: 8, paddingBottom: Platform.OS === 'ios' ? 120 : 100, }} onScroll={(event) => { scrollY.value = event.nativeEvent.contentOffset.y; }} scrollEventThrottle={32} showsVerticalScrollIndicator={false} onEndReached={handleLoadMore} onEndReachedThreshold={0.8} removeClippedSubviews={false} initialNumToRender={30} maxToRenderPerBatch={20} windowSize={7} ListFooterComponent={ displayLimit < filteredAndSortedMovies.length ? ( {isLoadingMore ? ( ) : ( {t('cast.load_more_remaining', { count: filteredAndSortedMovies.length - displayLimit })} )} ) : null } ListEmptyComponent={ {t('catalog.no_content_found')} {sortBy === 'upcoming' ? t('cast.no_upcoming') : selectedFilter === 'all' ? t('cast.no_content') : selectedFilter === 'movies' ? t('cast.no_movies') : t('cast.no_tv') } } /> )} {/* Inject CustomAlert component to display errors */} setAlertVisible(false)} /> ); }; export default CastMoviesScreen;