From 19bb898b6bd260afac960100f67254d86e76bc42 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sun, 8 Jun 2025 20:01:27 +0530 Subject: [PATCH] Enhance FeaturedContent component with improved animations and content transition handling. Introduce new animation states for poster scaling, overlay opacity, and content visibility during content changes. Refactor image loading logic to support smoother transitions and add a subtle content overlay for better readability. Update styles for improved layout and proportions. --- src/components/home/FeaturedContent.tsx | 362 +++++++++++++++--------- 1 file changed, 227 insertions(+), 135 deletions(-) diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index 2f88399df..858d60bdc 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -64,9 +64,18 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat // Add a ref to track logo fetch in progress const logoFetchInProgress = useRef(false); + // Enhanced poster transition animations + const posterScale = useSharedValue(1); + const posterTranslateY = useSharedValue(0); + const overlayOpacity = useSharedValue(0.15); + // Animation values const posterAnimatedStyle = useAnimatedStyle(() => ({ opacity: posterOpacity.value, + transform: [ + { scale: posterScale.value }, + { translateY: posterTranslateY.value } + ], })); const logoAnimatedStyle = useAnimatedStyle(() => ({ @@ -84,6 +93,10 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat opacity: buttonsOpacity.value, })); + const overlayAnimatedStyle = useAnimatedStyle(() => ({ + opacity: overlayOpacity.value, + })); + // Preload the image const preloadImage = async (url: string): Promise => { if (!url) return false; @@ -255,41 +268,92 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat const posterUrl = featuredContent.banner || featuredContent.poster; const contentId = featuredContent.id; + const isContentChange = contentId !== prevContentIdRef.current; - // Reset states for new content - if (contentId !== prevContentIdRef.current) { - posterOpacity.value = 0; + // Enhanced content change detection and animations + if (isContentChange) { + // Animate out current content + if (prevContentIdRef.current) { + posterOpacity.value = withTiming(0, { + duration: 300, + easing: Easing.out(Easing.cubic) + }); + posterScale.value = withTiming(0.95, { + duration: 300, + easing: Easing.out(Easing.cubic) + }); + overlayOpacity.value = withTiming(0.6, { + duration: 300, + easing: Easing.out(Easing.cubic) + }); + contentOpacity.value = withTiming(0.3, { + duration: 200, + easing: Easing.out(Easing.cubic) + }); + buttonsOpacity.value = withTiming(0.3, { + duration: 200, + easing: Easing.out(Easing.cubic) + }); + } else { + // Initial load - start from 0 + posterOpacity.value = 0; + posterScale.value = 1.1; + overlayOpacity.value = 0; + contentOpacity.value = 0; + buttonsOpacity.value = 0; + } logoOpacity.value = 0; } prevContentIdRef.current = contentId; - // Set poster URL immediately for instant display + // Set poster URL for immediate display if (posterUrl) setBannerUrl(posterUrl); - // Load images in background + // Load images with enhanced animations const loadImages = async () => { - // Load poster + // Small delay to allow fade out animation to complete + await new Promise(resolve => setTimeout(resolve, isContentChange && prevContentIdRef.current ? 300 : 0)); + + // Load poster with enhanced transition if (posterUrl) { const posterSuccess = await preloadImage(posterUrl); if (posterSuccess) { - posterOpacity.value = withTiming(1, { - duration: 600, - easing: Easing.bezier(0.25, 0.1, 0.25, 1) + // Animate in new poster with scale and fade + posterScale.value = withTiming(1, { + duration: 800, + easing: Easing.out(Easing.cubic) }); + posterOpacity.value = withTiming(1, { + duration: 700, + easing: Easing.out(Easing.cubic) + }); + overlayOpacity.value = withTiming(0.15, { + duration: 600, + easing: Easing.out(Easing.cubic) + }); + + // Animate content back in with delay + contentOpacity.value = withDelay(200, withTiming(1, { + duration: 600, + easing: Easing.out(Easing.cubic) + })); + buttonsOpacity.value = withDelay(400, withTiming(1, { + duration: 500, + easing: Easing.out(Easing.cubic) + })); } } - // Load logo if available + // Load logo if available with enhanced timing if (logoUrl) { const logoSuccess = await preloadImage(logoUrl); if (logoSuccess) { - logoOpacity.value = withDelay(300, withTiming(1, { - duration: 500, - easing: Easing.bezier(0.25, 0.1, 0.25, 1) + logoOpacity.value = withDelay(500, withTiming(1, { + duration: 600, + easing: Easing.out(Easing.cubic) })); } else { - // If prefetch fails, mark as error to show title text instead setLogoLoadError(true); console.warn(`[FeaturedContent] Logo prefetch failed, falling back to text: ${logoUrl}`); } @@ -304,131 +368,146 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat } return ( - { - navigation.navigate('Metadata', { - id: featuredContent.id, - type: featuredContent.type - }); - }} - style={styles.featuredContainer as ViewStyle} + - - - { + navigation.navigate('Metadata', { + id: featuredContent.id, + type: featuredContent.type + }); + }} + style={styles.featuredContainer as ViewStyle} + > + + - + + - {logoUrl && !logoLoadError ? ( - - { - console.warn(`[FeaturedContent] Logo failed to load: ${logoUrl}`); - setLogoLoadError(true); - }} + + {logoUrl && !logoLoadError ? ( + + { + console.warn(`[FeaturedContent] Logo failed to load: ${logoUrl}`); + setLogoLoadError(true); + }} + /> + + ) : ( + + {featuredContent.name} + + )} + + {featuredContent.genres?.slice(0, 3).map((genre, index, array) => ( + + + {genre} + + {index < array.length - 1 && ( + + )} + + ))} + + + + + + - - ) : ( - - {featuredContent.name} - - )} - - {featuredContent.genres?.slice(0, 3).map((genre, index, array) => ( - - - {genre} - - {index < array.length - 1 && ( - - )} - - ))} - - + + {isSaved ? "Saved" : "Save"} + + + + { + if (featuredContent) { + navigation.navigate('Streams', { + id: featuredContent.id, + type: featuredContent.type + }); + } + }} + > + + + Play + + - - - - - {isSaved ? "Saved" : "Save"} - - - - { - if (featuredContent) { - navigation.navigate('Streams', { - id: featuredContent.id, - type: featuredContent.type - }); - } - }} - > - - - Play - - - - { - if (featuredContent) { - navigation.navigate('Metadata', { - id: featuredContent.id, - type: featuredContent.type - }); - } - }} - > - - - Info - - - - - - - + { + if (featuredContent) { + navigation.navigate('Metadata', { + id: featuredContent.id, + type: featuredContent.type + }); + } + }} + > + + + Info + + + + + + + + ); }; const styles = StyleSheet.create({ featuredContainer: { width: '100%', - height: height * 0.48, + height: height * 0.55, // Slightly taller for better proportions marginTop: 0, - marginBottom: 8, + marginBottom: 12, position: 'relative', + borderRadius: 12, + overflow: 'hidden', + elevation: 8, + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.3, + shadowRadius: 8, }, imageContainer: { width: '100%', @@ -443,6 +522,7 @@ const styles = StyleSheet.create({ featuredImage: { width: '100%', height: '100%', + transform: [{ scale: 1.05 }], // Subtle zoom for depth }, backgroundFallback: { position: 'absolute', @@ -458,12 +538,14 @@ const styles = StyleSheet.create({ width: '100%', height: '100%', justifyContent: 'space-between', + paddingTop: 20, }, featuredContentContainer: { flex: 1, justifyContent: 'flex-end', - paddingHorizontal: 16, - paddingBottom: 4, + paddingHorizontal: 20, + paddingBottom: 8, + paddingTop: 40, }, featuredLogo: { width: width * 0.7, @@ -502,19 +584,20 @@ const styles = StyleSheet.create({ }, featuredButtons: { flexDirection: 'row', - alignItems: 'flex-end', + alignItems: 'center', justifyContent: 'space-evenly', width: '100%', - flex: 1, - maxHeight: 55, - paddingTop: 0, + minHeight: 70, + paddingTop: 12, + paddingBottom: 20, + paddingHorizontal: 8, }, playButton: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - paddingVertical: 14, - paddingHorizontal: 32, + paddingVertical: 12, + paddingHorizontal: 28, borderRadius: 30, elevation: 4, shadowColor: '#000', @@ -522,7 +605,7 @@ const styles = StyleSheet.create({ shadowOpacity: 0.3, shadowRadius: 4, flex: 0, - width: 150, + width: 140, }, myListButton: { flexDirection: 'column', @@ -557,6 +640,15 @@ const styles = StyleSheet.create({ fontSize: 12, fontWeight: '500', }, + contentOverlay: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0,0,0,0.15)', + zIndex: 1, + }, }); export default FeaturedContent; \ No newline at end of file