diff --git a/src/components/player/AndroidVideoPlayer.tsx b/src/components/player/AndroidVideoPlayer.tsx index d6b48707..ebde55fd 100644 --- a/src/components/player/AndroidVideoPlayer.tsx +++ b/src/components/player/AndroidVideoPlayer.tsx @@ -196,6 +196,8 @@ const AndroidVideoPlayer: React.FC = () => { const pauseOverlayTimerRef = useRef(null); const pauseOverlayOpacity = useRef(new Animated.Value(0)).current; const pauseOverlayTranslateY = useRef(new Animated.Value(12)).current; + const metadataOpacity = useRef(new Animated.Value(1)).current; + const metadataScale = useRef(new Animated.Value(1)).current; // Next episode button state const [showNextEpisodeButton, setShowNextEpisodeButton] = useState(false); @@ -205,11 +207,18 @@ const AndroidVideoPlayer: React.FC = () => { const [nextLoadingTitle, setNextLoadingTitle] = useState(null); const nextEpisodeButtonOpacity = useRef(new Animated.Value(0)).current; const nextEpisodeButtonScale = useRef(new Animated.Value(0.8)).current; + + // Cast display state + const [selectedCastMember, setSelectedCastMember] = useState(null); + const [showCastDetails, setShowCastDetails] = useState(false); + const castDetailsOpacity = useRef(new Animated.Value(0)).current; + const castDetailsScale = useRef(new Animated.Value(0.95)).current; + // Get metadata to access logo (only if we have a valid id) const shouldLoadMetadata = Boolean(id && type); const metadataResult = useMetadata({ id: id || 'placeholder', type: (type as any) }); const { settings: appSettings } = useSettings(); - const { metadata, loading: metadataLoading, groupedEpisodes } = shouldLoadMetadata ? (metadataResult as any) : { metadata: null, loading: false, groupedEpisodes: {} }; + const { metadata, loading: metadataLoading, groupedEpisodes, cast, loadCast } = shouldLoadMetadata ? (metadataResult as any) : { metadata: null, loading: false, groupedEpisodes: {}, cast: [], loadCast: () => {} }; // Logo animation values const logoScaleAnim = useRef(new Animated.Value(0.8)).current; @@ -1375,6 +1384,34 @@ const AndroidVideoPlayer: React.FC = () => { // Function to hide pause overlay and show controls const hidePauseOverlay = useCallback(() => { if (showPauseOverlay) { + // Reset cast details state when hiding overlay + if (showCastDetails) { + Animated.parallel([ + Animated.timing(castDetailsOpacity, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + Animated.timing(castDetailsScale, { + toValue: 0.95, + duration: 200, + useNativeDriver: true, + }) + ]).start(() => { + setShowCastDetails(false); + setSelectedCastMember(null); + // Reset metadata animations + metadataOpacity.setValue(1); + metadataScale.setValue(1); + }); + } else { + setShowCastDetails(false); + setSelectedCastMember(null); + // Reset metadata animations + metadataOpacity.setValue(1); + metadataScale.setValue(1); + } + Animated.parallel([ Animated.timing(pauseOverlayOpacity, { toValue: 0, @@ -1442,7 +1479,7 @@ const AndroidVideoPlayer: React.FC = () => { pauseOverlayTimerRef.current = null; } }; - }, [paused, hidePauseOverlay]); + }, [paused]); // Handle next episode button visibility based on current time and next episode availability useEffect(() => { @@ -1990,27 +2027,256 @@ const AndroidVideoPlayer: React.FC = () => { position: 'absolute', left: 24 + insets.left, right: 24 + insets.right, + top: 24 + insets.top, bottom: 110 + insets.bottom, transform: [{ translateY: pauseOverlayTranslateY }] }}> - You're watching - - {title} - - {!!year && ( - - {`${year}${type === 'series' && season && episode ? ` • S${season}E${episode}` : ''}`} - - )} - {!!episodeTitle && ( - - {episodeTitle} - - )} - {(currentEpisodeDescription || metadata?.description) && ( - - {(type as any) === 'series' ? (currentEpisodeDescription || metadata?.description || '') : (metadata?.description || '')} - + {showCastDetails && selectedCastMember ? ( + // Cast Detail View with fade transition + + + { + // Animate cast details out, then metadata back in + Animated.parallel([ + Animated.timing(castDetailsOpacity, { + toValue: 0, + duration: 250, + useNativeDriver: true, + }), + Animated.timing(castDetailsScale, { + toValue: 0.95, + duration: 250, + useNativeDriver: true, + }) + ]).start(() => { + setShowCastDetails(false); + setSelectedCastMember(null); + // Animate metadata back in + Animated.parallel([ + Animated.timing(metadataOpacity, { + toValue: 1, + duration: 400, + useNativeDriver: true, + }), + Animated.spring(metadataScale, { + toValue: 1, + tension: 80, + friction: 8, + useNativeDriver: true, + }) + ]).start(); + }); + }} + > + + Back to details + + + + {selectedCastMember.profile_path && ( + + + + )} + + + {selectedCastMember.name} + + {selectedCastMember.character && ( + + as {selectedCastMember.character} + + )} + + {/* Biography if available */} + {selectedCastMember.biography && ( + + {selectedCastMember.biography} + + )} + + + + + ) : ( + // Default Metadata View + + + You're watching + + {title} + + {!!year && ( + + {`${year}${type === 'series' && season && episode ? ` • S${season}E${episode}` : ''}`} + + )} + {!!episodeTitle && ( + + {episodeTitle} + + )} + {(currentEpisodeDescription || metadata?.description) && ( + + {(type as any) === 'series' ? (currentEpisodeDescription || metadata?.description || '') : (metadata?.description || '')} + + )} + {cast && cast.length > 0 && ( + + Cast + + {cast.slice(0, 6).map((castMember: any, index: number) => ( + { + setSelectedCastMember(castMember); + // Animate metadata out, then cast details in + Animated.parallel([ + Animated.timing(metadataOpacity, { + toValue: 0, + duration: 250, + useNativeDriver: true, + }), + Animated.timing(metadataScale, { + toValue: 0.95, + duration: 250, + useNativeDriver: true, + }) + ]).start(() => { + setShowCastDetails(true); + // Animate cast details in + Animated.parallel([ + Animated.timing(castDetailsOpacity, { + toValue: 1, + duration: 400, + useNativeDriver: true, + }), + Animated.spring(castDetailsScale, { + toValue: 1, + tension: 80, + friction: 8, + useNativeDriver: true, + }) + ]).start(); + }); + }} + > + + {castMember.name} + + + ))} + + + )} + + )} diff --git a/src/components/player/VideoPlayer.tsx b/src/components/player/VideoPlayer.tsx index 88d38f98..90ef8c20 100644 --- a/src/components/player/VideoPlayer.tsx +++ b/src/components/player/VideoPlayer.tsx @@ -216,6 +216,8 @@ const VideoPlayer: React.FC = () => { const pauseOverlayTimerRef = useRef(null); const pauseOverlayOpacity = useRef(new Animated.Value(0)).current; const pauseOverlayTranslateY = useRef(new Animated.Value(12)).current; + const metadataOpacity = useRef(new Animated.Value(1)).current; + const metadataScale = useRef(new Animated.Value(1)).current; // Next episode button state const [showNextEpisodeButton, setShowNextEpisodeButton] = useState(false); @@ -226,13 +228,19 @@ const VideoPlayer: React.FC = () => { const nextEpisodeButtonOpacity = useRef(new Animated.Value(0)).current; const nextEpisodeButtonScale = useRef(new Animated.Value(0.8)).current; + // Cast display state + const [selectedCastMember, setSelectedCastMember] = useState(null); + const [showCastDetails, setShowCastDetails] = useState(false); + const castDetailsOpacity = useRef(new Animated.Value(0)).current; + const castDetailsScale = useRef(new Animated.Value(0.95)).current; + // Get metadata to access logo (only if we have a valid id) const shouldLoadMetadata = Boolean(id && type); const metadataResult = useMetadata({ id: id || 'placeholder', type: type || 'movie' }); - const { metadata, loading: metadataLoading, groupedEpisodes } = shouldLoadMetadata ? (metadataResult as any) : { metadata: null, loading: false, groupedEpisodes: {} }; + const { metadata, loading: metadataLoading, groupedEpisodes, cast, loadCast } = shouldLoadMetadata ? (metadataResult as any) : { metadata: null, loading: false, groupedEpisodes: {}, cast: [], loadCast: () => {} }; const { settings } = useSettings(); // Logo animation values @@ -1275,6 +1283,34 @@ const VideoPlayer: React.FC = () => { // Function to hide pause overlay and show controls const hidePauseOverlay = useCallback(() => { if (showPauseOverlay) { + // Reset cast details state when hiding overlay + if (showCastDetails) { + Animated.parallel([ + Animated.timing(castDetailsOpacity, { + toValue: 0, + duration: 200, + useNativeDriver: true, + }), + Animated.timing(castDetailsScale, { + toValue: 0.95, + duration: 200, + useNativeDriver: true, + }) + ]).start(() => { + setShowCastDetails(false); + setSelectedCastMember(null); + // Reset metadata animations + metadataOpacity.setValue(1); + metadataScale.setValue(1); + }); + } else { + setShowCastDetails(false); + setSelectedCastMember(null); + // Reset metadata animations + metadataOpacity.setValue(1); + metadataScale.setValue(1); + } + Animated.parallel([ Animated.timing(pauseOverlayOpacity, { toValue: 0, @@ -1342,7 +1378,7 @@ const VideoPlayer: React.FC = () => { pauseOverlayTimerRef.current = null; } }; - }, [paused, hidePauseOverlay]); + }, [paused]); // Handle next episode button visibility based on current time and next episode availability useEffect(() => { @@ -1893,27 +1929,256 @@ const VideoPlayer: React.FC = () => { position: 'absolute', left: 24 + insets.left, right: 24 + insets.right, + top: 24 + insets.top, bottom: 110 + insets.bottom, transform: [{ translateY: pauseOverlayTranslateY }] }}> - You're watching - - {title} - - {!!year && ( - - {`${year}${type === 'series' && season && episode ? ` • S${season}E${episode}` : ''}`} - - )} - {!!episodeTitle && ( - - {episodeTitle} - - )} - {(currentEpisodeDescription || metadata?.description) && ( - - {type === 'series' ? (currentEpisodeDescription || metadata?.description || '') : (metadata?.description || '')} - + {showCastDetails && selectedCastMember ? ( + // Cast Detail View with fade transition + + + { + // Animate cast details out, then metadata back in + Animated.parallel([ + Animated.timing(castDetailsOpacity, { + toValue: 0, + duration: 250, + useNativeDriver: true, + }), + Animated.timing(castDetailsScale, { + toValue: 0.95, + duration: 250, + useNativeDriver: true, + }) + ]).start(() => { + setShowCastDetails(false); + setSelectedCastMember(null); + // Animate metadata back in + Animated.parallel([ + Animated.timing(metadataOpacity, { + toValue: 1, + duration: 400, + useNativeDriver: true, + }), + Animated.spring(metadataScale, { + toValue: 1, + tension: 80, + friction: 8, + useNativeDriver: true, + }) + ]).start(); + }); + }} + > + + Back to details + + + + {selectedCastMember.profile_path && ( + + + + )} + + + {selectedCastMember.name} + + {selectedCastMember.character && ( + + as {selectedCastMember.character} + + )} + + {/* Biography if available */} + {selectedCastMember.biography && ( + + {selectedCastMember.biography} + + )} + + + + + ) : ( + // Default Metadata View + + + You're watching + + {title} + + {!!year && ( + + {`${year}${type === 'series' && season && episode ? ` • S${season}E${episode}` : ''}`} + + )} + {!!episodeTitle && ( + + {episodeTitle} + + )} + {(currentEpisodeDescription || metadata?.description) && ( + + {type === 'series' ? (currentEpisodeDescription || metadata?.description || '') : (metadata?.description || '')} + + )} + {cast && cast.length > 0 && ( + + Cast + + {cast.slice(0, 6).map((castMember: any, index: number) => ( + { + setSelectedCastMember(castMember); + // Animate metadata out, then cast details in + Animated.parallel([ + Animated.timing(metadataOpacity, { + toValue: 0, + duration: 250, + useNativeDriver: true, + }), + Animated.timing(metadataScale, { + toValue: 0.95, + duration: 250, + useNativeDriver: true, + }) + ]).start(() => { + setShowCastDetails(true); + // Animate cast details in + Animated.parallel([ + Animated.timing(castDetailsOpacity, { + toValue: 1, + duration: 400, + useNativeDriver: true, + }), + Animated.spring(castDetailsScale, { + toValue: 1, + tension: 80, + friction: 8, + useNativeDriver: true, + }) + ]).start(); + }); + }} + > + + {castMember.name} + + + ))} + + + )} + + )}