import React, { useEffect, useMemo, useRef, useState } from 'react'; import { View, Text, TouchableOpacity, ActivityIndicator, Image, Platform } from 'react-native'; import { Animated } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import { logger } from '../../../utils/logger'; import { LinearGradient } from 'expo-linear-gradient'; export interface Insets { top: number; right: number; bottom: number; left: number; } export interface NextEpisodeLike { season_number: number; episode_number: number; name?: string; thumbnailUrl?: string; // Added thumbnailUrl to NextEpisodeLike } interface UpNextButtonProps { type: string | undefined; nextEpisode: NextEpisodeLike | null | undefined; currentTime: number; duration: number; insets: Insets; isLoading: boolean; nextLoadingProvider?: string | null; nextLoadingQuality?: string | null; nextLoadingTitle?: string | null; onPress: () => void; metadata?: { poster?: string; id?: string }; // Added metadata prop controlsVisible?: boolean; controlsFixedOffset?: number; } const UpNextButton: React.FC = ({ type, nextEpisode, currentTime, duration, insets, isLoading, nextLoadingProvider, nextLoadingQuality, nextLoadingTitle, onPress, metadata, controlsVisible = false, controlsFixedOffset = 100, }) => { const [visible, setVisible] = useState(false); const opacity = useRef(new Animated.Value(0)).current; const scale = useRef(new Animated.Value(0.8)).current; const translateY = useRef(new Animated.Value(0)).current; // Derive thumbnail similar to EpisodeCard let imageUri: string | null = null; const anyEpisode: any = nextEpisode as any; if (anyEpisode?.still_path) { if (typeof anyEpisode.still_path === 'string') { if (anyEpisode.still_path.startsWith('http')) { imageUri = anyEpisode.still_path; } else { try { const { tmdbService } = require('../../../services/tmdbService'); const url = tmdbService.getImageUrl(anyEpisode.still_path, 'w500'); if (url) imageUri = url; } catch { } } } } if (!imageUri && nextEpisode?.thumbnailUrl) imageUri = nextEpisode.thumbnailUrl; if (!imageUri && metadata?.poster) imageUri = metadata.poster || null; const shouldShow = useMemo(() => { if (!nextEpisode || duration <= 0) return false; const timeRemaining = duration - currentTime; // Be tolerant to timer jitter: show when under ~1 minute and above 10s return timeRemaining < 61 && timeRemaining > 10; }, [nextEpisode, duration, currentTime]); // Debug logging removed to reduce console noise // The state is computed in shouldShow useMemo above useEffect(() => { if (shouldShow && !visible) { try { logger.log('[UpNextButton] showing with animation'); } catch { } setVisible(true); Animated.parallel([ Animated.timing(opacity, { toValue: 1, duration: 400, useNativeDriver: true }), Animated.spring(scale, { toValue: 1, tension: 100, friction: 8, useNativeDriver: true }), ]).start(); } else if (!shouldShow && visible) { try { logger.log('[UpNextButton] hiding with animation'); } catch { } Animated.parallel([ Animated.timing(opacity, { toValue: 0, duration: 200, useNativeDriver: true }), Animated.timing(scale, { toValue: 0.8, duration: 200, useNativeDriver: true }), ]).start(() => { setVisible(false); }); } }, [shouldShow, visible, opacity, scale]); // Animate vertical offset based on controls visibility useEffect(() => { // Android needs more offset to clear the slider const androidOffset = controlsFixedOffset - 8; const iosOffset = controlsFixedOffset / 2; const target = controlsVisible ? -(Platform.OS === 'android' ? androidOffset : iosOffset) : 0; Animated.timing(translateY, { toValue: target, duration: 220, useNativeDriver: true, }).start(); }, [controlsVisible, controlsFixedOffset, translateY]); if (!visible || !nextEpisode) return null; return ( {/* Thumbnail fills card */} {imageUri ? ( ) : ( )} {/* Bottom overlay text */} {isLoading ? ( ) : ( )} {isLoading ? 'Loading next…' : 'Up next'} S{nextEpisode.season_number}E{nextEpisode.episode_number} {nextEpisode.name ? `: ${nextEpisode.name}` : ''} {isLoading && ( {nextLoadingProvider ? `${nextLoadingProvider}` : 'Finding source…'} {nextLoadingQuality ? ` • ${nextLoadingQuality}p` : ''} {nextLoadingTitle ? ` • ${nextLoadingTitle}` : ''} )} ); } export default UpNextButton;