diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index 76d47a21..ccd025c1 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -60,13 +60,23 @@ const MovieContent = React.memo(OriginalMovieContent); const MoreLikeThisSection = React.memo(OriginalMoreLikeThisSection); const RatingsSection = React.memo(OriginalRatingsSection); -// Animation configs +// Animation constants const springConfig = { damping: 20, mass: 1, stiffness: 100 }; +// Animation timing constants for staggered appearance +const ANIMATION_DELAY_CONSTANTS = { + HERO: 100, + LOGO: 250, + PROGRESS: 350, + GENRES: 400, + BUTTONS: 450, + CONTENT: 500 +}; + // Add debug log for storageService logger.log('[MetadataScreen] StorageService instance:', storageService); @@ -78,7 +88,8 @@ const ActionButtons = React.memo(({ type, id, navigation, - playButtonText + playButtonText, + animatedStyle }: { handleShowStreams: () => void; toggleLibrary: () => void; @@ -87,8 +98,9 @@ const ActionButtons = React.memo(({ id: string; navigation: NavigationProp; playButtonText: string; + animatedStyle: any; }) => ( - + )} - + )); // Memoized WatchProgress Component @@ -212,10 +224,18 @@ const MetadataScreen = () => { const [isFullDescriptionOpen, setIsFullDescriptionOpen] = useState(false); // Animation values - const screenScale = useSharedValue(0.8); + const screenScale = useSharedValue(0.92); const screenOpacity = useSharedValue(0); const heroHeight = useSharedValue(height * 0.5); - const contentTranslateY = useSharedValue(50); + const contentTranslateY = useSharedValue(60); + + // Additional animation values for staggered entrance + const heroScale = useSharedValue(1.05); + const heroOpacity = useSharedValue(0); + const genresOpacity = useSharedValue(0); + const genresTranslateY = useSharedValue(20); + const buttonsOpacity = useSharedValue(0); + const buttonsTranslateY = useSharedValue(30); // Add state for watch progress const [watchProgress, setWatchProgress] = useState<{ @@ -231,6 +251,7 @@ const MetadataScreen = () => { // Add animated value for logo const logoOpacity = useSharedValue(0); + const logoScale = useSharedValue(0.9); // Debug log for route params // logger.log('[MetadataScreen] Component mounted with route params:', { id, type, episodeId }); @@ -498,12 +519,7 @@ const MetadataScreen = () => { const logoAnimatedStyle = useAnimatedStyle(() => { return { opacity: logoOpacity.value, - transform: [{ scale: interpolate( - logoOpacity.value, - [0, 1], - [0.95, 1], - Extrapolate.CLAMP - ) }], + transform: [{ scale: logoScale.value }] }; }); @@ -580,19 +596,37 @@ const MetadataScreen = () => { const heroAnimatedStyle = useAnimatedStyle(() => ({ width: '100%', height: heroHeight.value, - backgroundColor: colors.black + backgroundColor: colors.black, + transform: [{ scale: heroScale.value }], + opacity: heroOpacity.value })); const contentAnimatedStyle = useAnimatedStyle(() => ({ transform: [{ translateY: contentTranslateY.value }], opacity: interpolate( contentTranslateY.value, - [50, 0], + [60, 0], [0, 1], Extrapolate.CLAMP ) })); + // Add animated style for genres + const genresAnimatedStyle = useAnimatedStyle(() => { + return { + opacity: genresOpacity.value, + transform: [{ translateY: genresTranslateY.value }] + }; + }); + + // Add animated style for buttons + const buttonsAnimatedStyle = useAnimatedStyle(() => { + return { + opacity: buttonsOpacity.value, + transform: [{ translateY: buttonsTranslateY.value }] + }; + }); + // Debug logs for director/creator data React.useEffect(() => { if (metadata && metadata.id) { @@ -653,13 +687,85 @@ const MetadataScreen = () => { // Start entrance animation React.useEffect(() => { - screenScale.value = withSpring(1, springConfig); - screenOpacity.value = withSpring(1, springConfig); - contentTranslateY.value = withSpring(0, { - damping: 25, - mass: 1, - stiffness: 100 - }); + // Use a timeout to ensure the animations starts after the component is mounted + const animationTimeout = setTimeout(() => { + // 1. First animate the container + screenScale.value = withSpring(1, springConfig); + screenOpacity.value = withSpring(1, springConfig); + + // 2. Then animate the hero section with a slight delay + setTimeout(() => { + heroOpacity.value = withSpring(1, { + damping: 14, + stiffness: 80 + }); + heroScale.value = withSpring(1, { + damping: 18, + stiffness: 100 + }); + }, ANIMATION_DELAY_CONSTANTS.HERO); + + // 3. Then animate the logo + setTimeout(() => { + logoOpacity.value = withSpring(1, { + damping: 12, + stiffness: 100 + }); + logoScale.value = withSpring(1, { + damping: 14, + stiffness: 90 + }); + }, ANIMATION_DELAY_CONSTANTS.LOGO); + + // 4. Then animate the watch progress if applicable + setTimeout(() => { + if (watchProgress && watchProgress.duration > 0) { + watchProgressOpacity.value = withSpring(1, { + damping: 14, + stiffness: 100 + }); + watchProgressScaleY.value = withSpring(1, { + damping: 18, + stiffness: 120 + }); + } + }, ANIMATION_DELAY_CONSTANTS.PROGRESS); + + // 5. Then animate the genres + setTimeout(() => { + genresOpacity.value = withSpring(1, { + damping: 14, + stiffness: 100 + }); + genresTranslateY.value = withSpring(0, { + damping: 18, + stiffness: 120 + }); + }, ANIMATION_DELAY_CONSTANTS.GENRES); + + // 6. Then animate the buttons + setTimeout(() => { + buttonsOpacity.value = withSpring(1, { + damping: 14, + stiffness: 100 + }); + buttonsTranslateY.value = withSpring(0, { + damping: 18, + stiffness: 120 + }); + }, ANIMATION_DELAY_CONSTANTS.BUTTONS); + + // 7. Finally animate the content section + setTimeout(() => { + contentTranslateY.value = withSpring(0, { + damping: 25, + mass: 1, + stiffness: 100 + }); + }, ANIMATION_DELAY_CONSTANTS.CONTENT); + }, 50); // Small timeout to ensure component is fully mounted + + return () => clearTimeout(animationTimeout); }, []); const handleBack = useCallback(() => { @@ -679,20 +785,16 @@ const MetadataScreen = () => { // Since metadata.genres is string[], we display them directly const genresToDisplay: string[] = metadata.genres as string[]; - return ( - - {genresToDisplay.slice(0, 4).map((genreName, index, array) => ( - // Use React.Fragment to avoid extra View wrappers - - {genreName} - {/* Add dot separator */} - {index < array.length - 1 && ( - - )} - - ))} - - ); + return genresToDisplay.slice(0, 4).map((genreName, index, array) => ( + // Use React.Fragment to avoid extra View wrappers + + {genreName} + {/* Add dot separator */} + {index < array.length - 1 && ( + + )} + + )); }, [metadata?.genres]); // Dependency on metadata.genres if (loading) { @@ -834,7 +936,11 @@ const MetadataScreen = () => { /> {/* Genre Tags */} - {renderGenres} + + + {renderGenres} + + {/* Action Buttons */} { id={id} navigation={navigation} playButtonText={getPlayButtonText()} + animatedStyle={buttonsAnimatedStyle} />