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.

This commit is contained in:
tapframe 2025-06-08 20:01:27 +05:30
parent 076446aac6
commit 19bb898b6b

View file

@ -64,9 +64,18 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
// Add a ref to track logo fetch in progress // Add a ref to track logo fetch in progress
const logoFetchInProgress = useRef<boolean>(false); const logoFetchInProgress = useRef<boolean>(false);
// Enhanced poster transition animations
const posterScale = useSharedValue(1);
const posterTranslateY = useSharedValue(0);
const overlayOpacity = useSharedValue(0.15);
// Animation values // Animation values
const posterAnimatedStyle = useAnimatedStyle(() => ({ const posterAnimatedStyle = useAnimatedStyle(() => ({
opacity: posterOpacity.value, opacity: posterOpacity.value,
transform: [
{ scale: posterScale.value },
{ translateY: posterTranslateY.value }
],
})); }));
const logoAnimatedStyle = useAnimatedStyle(() => ({ const logoAnimatedStyle = useAnimatedStyle(() => ({
@ -84,6 +93,10 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
opacity: buttonsOpacity.value, opacity: buttonsOpacity.value,
})); }));
const overlayAnimatedStyle = useAnimatedStyle(() => ({
opacity: overlayOpacity.value,
}));
// Preload the image // Preload the image
const preloadImage = async (url: string): Promise<boolean> => { const preloadImage = async (url: string): Promise<boolean> => {
if (!url) return false; if (!url) return false;
@ -255,41 +268,92 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
const posterUrl = featuredContent.banner || featuredContent.poster; const posterUrl = featuredContent.banner || featuredContent.poster;
const contentId = featuredContent.id; const contentId = featuredContent.id;
const isContentChange = contentId !== prevContentIdRef.current;
// Reset states for new content // Enhanced content change detection and animations
if (contentId !== prevContentIdRef.current) { if (isContentChange) {
posterOpacity.value = 0; // 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; logoOpacity.value = 0;
} }
prevContentIdRef.current = contentId; prevContentIdRef.current = contentId;
// Set poster URL immediately for instant display // Set poster URL for immediate display
if (posterUrl) setBannerUrl(posterUrl); if (posterUrl) setBannerUrl(posterUrl);
// Load images in background // Load images with enhanced animations
const loadImages = async () => { 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) { if (posterUrl) {
const posterSuccess = await preloadImage(posterUrl); const posterSuccess = await preloadImage(posterUrl);
if (posterSuccess) { if (posterSuccess) {
posterOpacity.value = withTiming(1, { // Animate in new poster with scale and fade
duration: 600, posterScale.value = withTiming(1, {
easing: Easing.bezier(0.25, 0.1, 0.25, 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) { if (logoUrl) {
const logoSuccess = await preloadImage(logoUrl); const logoSuccess = await preloadImage(logoUrl);
if (logoSuccess) { if (logoSuccess) {
logoOpacity.value = withDelay(300, withTiming(1, { logoOpacity.value = withDelay(500, withTiming(1, {
duration: 500, duration: 600,
easing: Easing.bezier(0.25, 0.1, 0.25, 1) easing: Easing.out(Easing.cubic)
})); }));
} else { } else {
// If prefetch fails, mark as error to show title text instead
setLogoLoadError(true); setLogoLoadError(true);
console.warn(`[FeaturedContent] Logo prefetch failed, falling back to text: ${logoUrl}`); console.warn(`[FeaturedContent] Logo prefetch failed, falling back to text: ${logoUrl}`);
} }
@ -304,131 +368,146 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
} }
return ( return (
<TouchableOpacity <Animated.View
activeOpacity={0.9} entering={FadeIn.duration(800).easing(Easing.out(Easing.cubic))}
onPress={() => {
navigation.navigate('Metadata', {
id: featuredContent.id,
type: featuredContent.type
});
}}
style={styles.featuredContainer as ViewStyle}
> >
<Animated.View style={[styles.imageContainer, posterAnimatedStyle]}> <TouchableOpacity
<ImageBackground activeOpacity={0.95}
source={{ uri: bannerUrl || featuredContent.poster }} onPress={() => {
style={styles.featuredImage as ViewStyle} navigation.navigate('Metadata', {
resizeMode="cover" id: featuredContent.id,
> type: featuredContent.type
<LinearGradient });
colors={[ }}
'transparent', style={styles.featuredContainer as ViewStyle}
'rgba(0,0,0,0.1)', >
'rgba(0,0,0,0.7)', <Animated.View style={[styles.imageContainer, posterAnimatedStyle]}>
currentTheme.colors.darkBackground, <ImageBackground
]} source={{ uri: bannerUrl || featuredContent.poster }}
locations={[0, 0.3, 0.7, 1]} style={styles.featuredImage as ViewStyle}
style={styles.featuredGradient as ViewStyle} resizeMode="cover"
> >
<Animated.View {/* Subtle content overlay for better readability */}
style={[styles.featuredContentContainer as ViewStyle, contentAnimatedStyle]} <Animated.View style={[styles.contentOverlay, overlayAnimatedStyle]} />
<LinearGradient
colors={[
'rgba(0,0,0,0.1)',
'rgba(0,0,0,0.2)',
'rgba(0,0,0,0.4)',
'rgba(0,0,0,0.8)',
currentTheme.colors.darkBackground,
]}
locations={[0, 0.2, 0.5, 0.8, 1]}
style={styles.featuredGradient as ViewStyle}
> >
{logoUrl && !logoLoadError ? ( <Animated.View
<Animated.View style={logoAnimatedStyle}> style={[styles.featuredContentContainer as ViewStyle, contentAnimatedStyle]}
<ExpoImage >
source={{ uri: logoUrl }} {logoUrl && !logoLoadError ? (
style={styles.featuredLogo as ImageStyle} <Animated.View style={logoAnimatedStyle}>
contentFit="contain" <ExpoImage
cachePolicy="memory-disk" source={{ uri: logoUrl }}
transition={400} style={styles.featuredLogo as ImageStyle}
onError={() => { contentFit="contain"
console.warn(`[FeaturedContent] Logo failed to load: ${logoUrl}`); cachePolicy="memory-disk"
setLogoLoadError(true); transition={400}
}} onError={() => {
console.warn(`[FeaturedContent] Logo failed to load: ${logoUrl}`);
setLogoLoadError(true);
}}
/>
</Animated.View>
) : (
<Text style={[styles.featuredTitleText as TextStyle, { color: currentTheme.colors.highEmphasis }]}>
{featuredContent.name}
</Text>
)}
<View style={styles.genreContainer as ViewStyle}>
{featuredContent.genres?.slice(0, 3).map((genre, index, array) => (
<React.Fragment key={index}>
<Text style={[styles.genreText as TextStyle, { color: currentTheme.colors.white }]}>
{genre}
</Text>
{index < array.length - 1 && (
<Text style={[styles.genreDot as TextStyle, { color: currentTheme.colors.white }]}></Text>
)}
</React.Fragment>
))}
</View>
</Animated.View>
<Animated.View style={[styles.featuredButtons as ViewStyle, buttonsAnimatedStyle]}>
<TouchableOpacity
style={styles.myListButton as ViewStyle}
onPress={handleSaveToLibrary}
>
<MaterialIcons
name={isSaved ? "bookmark" : "bookmark-border"}
size={24}
color={currentTheme.colors.white}
/> />
</Animated.View> <Text style={[styles.myListButtonText as TextStyle, { color: currentTheme.colors.white }]}>
) : ( {isSaved ? "Saved" : "Save"}
<Text style={[styles.featuredTitleText as TextStyle, { color: currentTheme.colors.highEmphasis }]}> </Text>
{featuredContent.name} </TouchableOpacity>
</Text>
)} <TouchableOpacity
<View style={styles.genreContainer as ViewStyle}> style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
{featuredContent.genres?.slice(0, 3).map((genre, index, array) => ( onPress={() => {
<React.Fragment key={index}> if (featuredContent) {
<Text style={[styles.genreText as TextStyle, { color: currentTheme.colors.white }]}> navigation.navigate('Streams', {
{genre} id: featuredContent.id,
</Text> type: featuredContent.type
{index < array.length - 1 && ( });
<Text style={[styles.genreDot as TextStyle, { color: currentTheme.colors.white }]}></Text> }
)} }}
</React.Fragment> >
))} <MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
</View> <Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
</Animated.View> Play
</Text>
</TouchableOpacity>
<Animated.View style={[styles.featuredButtons as ViewStyle, buttonsAnimatedStyle]}> <TouchableOpacity
<TouchableOpacity style={styles.infoButton as ViewStyle}
style={styles.myListButton as ViewStyle} onPress={() => {
onPress={handleSaveToLibrary} if (featuredContent) {
> navigation.navigate('Metadata', {
<MaterialIcons id: featuredContent.id,
name={isSaved ? "bookmark" : "bookmark-border"} type: featuredContent.type
size={24} });
color={currentTheme.colors.white} }
/> }}
<Text style={[styles.myListButtonText as TextStyle, { color: currentTheme.colors.white }]}> >
{isSaved ? "Saved" : "Save"} <MaterialIcons name="info-outline" size={24} color={currentTheme.colors.white} />
</Text> <Text style={[styles.infoButtonText as TextStyle, { color: currentTheme.colors.white }]}>
</TouchableOpacity> Info
</Text>
<TouchableOpacity </TouchableOpacity>
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]} </Animated.View>
onPress={() => { </LinearGradient>
if (featuredContent) { </ImageBackground>
navigation.navigate('Streams', { </Animated.View>
id: featuredContent.id, </TouchableOpacity>
type: featuredContent.type </Animated.View>
});
}
}}
>
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
Play
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.infoButton as ViewStyle}
onPress={() => {
if (featuredContent) {
navigation.navigate('Metadata', {
id: featuredContent.id,
type: featuredContent.type
});
}
}}
>
<MaterialIcons name="info-outline" size={24} color={currentTheme.colors.white} />
<Text style={[styles.infoButtonText as TextStyle, { color: currentTheme.colors.white }]}>
Info
</Text>
</TouchableOpacity>
</Animated.View>
</LinearGradient>
</ImageBackground>
</Animated.View>
</TouchableOpacity>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
featuredContainer: { featuredContainer: {
width: '100%', width: '100%',
height: height * 0.48, height: height * 0.55, // Slightly taller for better proportions
marginTop: 0, marginTop: 0,
marginBottom: 8, marginBottom: 12,
position: 'relative', position: 'relative',
borderRadius: 12,
overflow: 'hidden',
elevation: 8,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
}, },
imageContainer: { imageContainer: {
width: '100%', width: '100%',
@ -443,6 +522,7 @@ const styles = StyleSheet.create({
featuredImage: { featuredImage: {
width: '100%', width: '100%',
height: '100%', height: '100%',
transform: [{ scale: 1.05 }], // Subtle zoom for depth
}, },
backgroundFallback: { backgroundFallback: {
position: 'absolute', position: 'absolute',
@ -458,12 +538,14 @@ const styles = StyleSheet.create({
width: '100%', width: '100%',
height: '100%', height: '100%',
justifyContent: 'space-between', justifyContent: 'space-between',
paddingTop: 20,
}, },
featuredContentContainer: { featuredContentContainer: {
flex: 1, flex: 1,
justifyContent: 'flex-end', justifyContent: 'flex-end',
paddingHorizontal: 16, paddingHorizontal: 20,
paddingBottom: 4, paddingBottom: 8,
paddingTop: 40,
}, },
featuredLogo: { featuredLogo: {
width: width * 0.7, width: width * 0.7,
@ -502,19 +584,20 @@ const styles = StyleSheet.create({
}, },
featuredButtons: { featuredButtons: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'flex-end', alignItems: 'center',
justifyContent: 'space-evenly', justifyContent: 'space-evenly',
width: '100%', width: '100%',
flex: 1, minHeight: 70,
maxHeight: 55, paddingTop: 12,
paddingTop: 0, paddingBottom: 20,
paddingHorizontal: 8,
}, },
playButton: { playButton: {
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
paddingVertical: 14, paddingVertical: 12,
paddingHorizontal: 32, paddingHorizontal: 28,
borderRadius: 30, borderRadius: 30,
elevation: 4, elevation: 4,
shadowColor: '#000', shadowColor: '#000',
@ -522,7 +605,7 @@ const styles = StyleSheet.create({
shadowOpacity: 0.3, shadowOpacity: 0.3,
shadowRadius: 4, shadowRadius: 4,
flex: 0, flex: 0,
width: 150, width: 140,
}, },
myListButton: { myListButton: {
flexDirection: 'column', flexDirection: 'column',
@ -557,6 +640,15 @@ const styles = StyleSheet.create({
fontSize: 12, fontSize: 12,
fontWeight: '500', 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; export default FeaturedContent;