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