mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-01-11 20:10:25 +00:00
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:
parent
076446aac6
commit
19bb898b6b
1 changed files with 227 additions and 135 deletions
|
|
@ -64,9 +64,18 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary }: Feat
|
|||
// Add a ref to track logo fetch in progress
|
||||
const logoFetchInProgress = useRef<boolean>(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<boolean> => {
|
||||
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 (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={() => {
|
||||
navigation.navigate('Metadata', {
|
||||
id: featuredContent.id,
|
||||
type: featuredContent.type
|
||||
});
|
||||
}}
|
||||
style={styles.featuredContainer as ViewStyle}
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(800).easing(Easing.out(Easing.cubic))}
|
||||
>
|
||||
<Animated.View style={[styles.imageContainer, posterAnimatedStyle]}>
|
||||
<ImageBackground
|
||||
source={{ uri: bannerUrl || featuredContent.poster }}
|
||||
style={styles.featuredImage as ViewStyle}
|
||||
resizeMode="cover"
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[
|
||||
'transparent',
|
||||
'rgba(0,0,0,0.1)',
|
||||
'rgba(0,0,0,0.7)',
|
||||
currentTheme.colors.darkBackground,
|
||||
]}
|
||||
locations={[0, 0.3, 0.7, 1]}
|
||||
style={styles.featuredGradient as ViewStyle}
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.95}
|
||||
onPress={() => {
|
||||
navigation.navigate('Metadata', {
|
||||
id: featuredContent.id,
|
||||
type: featuredContent.type
|
||||
});
|
||||
}}
|
||||
style={styles.featuredContainer as ViewStyle}
|
||||
>
|
||||
<Animated.View style={[styles.imageContainer, posterAnimatedStyle]}>
|
||||
<ImageBackground
|
||||
source={{ uri: bannerUrl || featuredContent.poster }}
|
||||
style={styles.featuredImage as ViewStyle}
|
||||
resizeMode="cover"
|
||||
>
|
||||
<Animated.View
|
||||
style={[styles.featuredContentContainer as ViewStyle, contentAnimatedStyle]}
|
||||
{/* Subtle content overlay for better readability */}
|
||||
<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 style={logoAnimatedStyle}>
|
||||
<ExpoImage
|
||||
source={{ uri: logoUrl }}
|
||||
style={styles.featuredLogo as ImageStyle}
|
||||
contentFit="contain"
|
||||
cachePolicy="memory-disk"
|
||||
transition={400}
|
||||
onError={() => {
|
||||
console.warn(`[FeaturedContent] Logo failed to load: ${logoUrl}`);
|
||||
setLogoLoadError(true);
|
||||
}}
|
||||
<Animated.View
|
||||
style={[styles.featuredContentContainer as ViewStyle, contentAnimatedStyle]}
|
||||
>
|
||||
{logoUrl && !logoLoadError ? (
|
||||
<Animated.View style={logoAnimatedStyle}>
|
||||
<ExpoImage
|
||||
source={{ uri: logoUrl }}
|
||||
style={styles.featuredLogo as ImageStyle}
|
||||
contentFit="contain"
|
||||
cachePolicy="memory-disk"
|
||||
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.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>
|
||||
<Text style={[styles.myListButtonText as TextStyle, { color: currentTheme.colors.white }]}>
|
||||
{isSaved ? "Saved" : "Save"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
|
||||
onPress={() => {
|
||||
if (featuredContent) {
|
||||
navigation.navigate('Streams', {
|
||||
id: featuredContent.id,
|
||||
type: featuredContent.type
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MaterialIcons name="play-arrow" size={24} color={currentTheme.colors.black} />
|
||||
<Text style={[styles.playButtonText as TextStyle, { color: currentTheme.colors.black }]}>
|
||||
Play
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<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}
|
||||
/>
|
||||
<Text style={[styles.myListButtonText as TextStyle, { color: currentTheme.colors.white }]}>
|
||||
{isSaved ? "Saved" : "Save"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.playButton as ViewStyle, { backgroundColor: currentTheme.colors.white }]}
|
||||
onPress={() => {
|
||||
if (featuredContent) {
|
||||
navigation.navigate('Streams', {
|
||||
id: featuredContent.id,
|
||||
type: featuredContent.type
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
Loading…
Reference in a new issue