mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +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
|
// 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;
|
||||||
Loading…
Reference in a new issue