mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-04-21 00:32:04 +00:00
Enhance HeroSection with improved image loading animations and error handling. Introduce state management for backdrop image transitions, including opacity and scale animations. Refactor image loading logic to provide a smoother user experience during content loading and error states, ensuring better visual feedback and responsiveness.
This commit is contained in:
parent
4a94e6248d
commit
da6eb659f1
1 changed files with 104 additions and 13 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import React from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -13,6 +13,9 @@ import Animated, {
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
interpolate,
|
interpolate,
|
||||||
Extrapolate,
|
Extrapolate,
|
||||||
|
useSharedValue,
|
||||||
|
withTiming,
|
||||||
|
withSpring,
|
||||||
} from 'react-native-reanimated';
|
} from 'react-native-reanimated';
|
||||||
import { useTheme } from '../../contexts/ThemeContext';
|
import { useTheme } from '../../contexts/ThemeContext';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
|
@ -259,6 +262,48 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
setLogoLoadError,
|
setLogoLoadError,
|
||||||
}) => {
|
}) => {
|
||||||
const { currentTheme } = useTheme();
|
const { currentTheme } = useTheme();
|
||||||
|
|
||||||
|
// State for backdrop image loading
|
||||||
|
const [imageLoaded, setImageLoaded] = useState(false);
|
||||||
|
const [imageError, setImageError] = useState(false);
|
||||||
|
|
||||||
|
// Animation values for smooth backdrop transitions
|
||||||
|
const backdropOpacity = useSharedValue(1); // Start visible
|
||||||
|
const backdropScale = useSharedValue(1); // Start at normal scale
|
||||||
|
|
||||||
|
// Handle image load success
|
||||||
|
const handleImageLoad = () => {
|
||||||
|
setImageLoaded(true);
|
||||||
|
setImageError(false);
|
||||||
|
// Enhance the image with subtle animation
|
||||||
|
backdropOpacity.value = withTiming(1, { duration: 300 });
|
||||||
|
backdropScale.value = withSpring(1, {
|
||||||
|
damping: 25,
|
||||||
|
stiffness: 120,
|
||||||
|
mass: 1
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle image load error
|
||||||
|
const handleImageError = () => {
|
||||||
|
logger.warn(`[HeroSection] Banner failed to load: ${bannerImage}`);
|
||||||
|
setImageError(true);
|
||||||
|
backdropOpacity.value = withTiming(0.7, { duration: 200 }); // Dim on error
|
||||||
|
if (bannerImage !== metadata.banner) {
|
||||||
|
setBannerImage(metadata.banner || metadata.poster);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset animations when banner image changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (bannerImage && !loadingBanner) {
|
||||||
|
setImageLoaded(false);
|
||||||
|
setImageError(false);
|
||||||
|
backdropOpacity.value = 0.8; // Start slightly dimmed
|
||||||
|
backdropScale.value = 0.98; // Start slightly smaller
|
||||||
|
}
|
||||||
|
}, [bannerImage, loadingBanner]);
|
||||||
|
|
||||||
// Enhanced animated styles with sophisticated micro-animations
|
// Enhanced animated styles with sophisticated micro-animations
|
||||||
const heroAnimatedStyle = useAnimatedStyle(() => ({
|
const heroAnimatedStyle = useAnimatedStyle(() => ({
|
||||||
width: '100%',
|
width: '100%',
|
||||||
|
|
@ -317,7 +362,6 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const watchProgressBarStyle = useAnimatedStyle(() => ({
|
const watchProgressBarStyle = useAnimatedStyle(() => ({
|
||||||
width: `${watchProgressWidth.value * 100}%`,
|
|
||||||
transform: [
|
transform: [
|
||||||
{ scaleX: interpolate(watchProgressWidth.value, [0, 1], [0.8, 1]) }
|
{ scaleX: interpolate(watchProgressWidth.value, [0, 1], [0.8, 1]) }
|
||||||
]
|
]
|
||||||
|
|
@ -373,7 +417,7 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
[0, 150, 300],
|
[0, 150, 300],
|
||||||
[1.08, 1.05, 1.02],
|
[1.08, 1.05, 1.02],
|
||||||
Extrapolate.CLAMP
|
Extrapolate.CLAMP
|
||||||
)
|
) * backdropScale.value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rotateZ: interpolate(
|
rotateZ: interpolate(
|
||||||
|
|
@ -386,6 +430,42 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Backdrop image animated style for smooth transitions
|
||||||
|
const backdropImageStyle = useAnimatedStyle(() => ({
|
||||||
|
opacity: backdropOpacity.value,
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
translateY: interpolate(
|
||||||
|
dampedScrollY.value,
|
||||||
|
[0, 100, 300],
|
||||||
|
[0, -35, -90],
|
||||||
|
Extrapolate.CLAMP
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
scale: interpolate(
|
||||||
|
dampedScrollY.value,
|
||||||
|
[0, 150, 300],
|
||||||
|
[1.08, 1.05, 1.02],
|
||||||
|
Extrapolate.CLAMP
|
||||||
|
) * backdropScale.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rotateZ: interpolate(
|
||||||
|
dampedScrollY.value,
|
||||||
|
[0, 300],
|
||||||
|
[0, -0.1],
|
||||||
|
Extrapolate.CLAMP
|
||||||
|
) + 'deg'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Loading skeleton animated style
|
||||||
|
const skeletonStyle = useAnimatedStyle(() => ({
|
||||||
|
opacity: loadingBanner ? 0.2 : 0,
|
||||||
|
}));
|
||||||
|
|
||||||
// Render genres
|
// Render genres
|
||||||
const renderGenres = () => {
|
const renderGenres = () => {
|
||||||
if (!metadata?.genres || !Array.isArray(metadata.genres) || metadata.genres.length === 0) {
|
if (!metadata?.genres || !Array.isArray(metadata.genres) || metadata.genres.length === 0) {
|
||||||
|
|
@ -411,19 +491,22 @@ const HeroSection: React.FC<HeroSectionProps> = ({
|
||||||
return (
|
return (
|
||||||
<Animated.View style={heroAnimatedStyle}>
|
<Animated.View style={heroAnimatedStyle}>
|
||||||
<View style={styles.heroSection}>
|
<View style={styles.heroSection}>
|
||||||
{loadingBanner ? (
|
{/* Fallback dark background */}
|
||||||
<View style={[styles.absoluteFill, { backgroundColor: currentTheme.colors.black }]} />
|
<View style={[styles.absoluteFill, { backgroundColor: currentTheme.colors.black }]} />
|
||||||
) : (
|
|
||||||
|
{/* Loading state with skeleton */}
|
||||||
|
{loadingBanner && (
|
||||||
|
<Animated.View style={[styles.absoluteFill, styles.skeletonGradient, skeletonStyle]} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Background image with smooth loading */}
|
||||||
|
{!loadingBanner && (bannerImage || metadata.banner || metadata.poster) && (
|
||||||
<Animated.Image
|
<Animated.Image
|
||||||
source={{ uri: bannerImage || metadata.banner || metadata.poster }}
|
source={{ uri: bannerImage || metadata.banner || metadata.poster }}
|
||||||
style={[styles.absoluteFill, parallaxImageStyle]}
|
style={[styles.absoluteFill, backdropImageStyle]}
|
||||||
resizeMode="cover"
|
resizeMode="cover"
|
||||||
onError={() => {
|
onError={handleImageError}
|
||||||
logger.warn(`[HeroSection] Banner failed to load: ${bannerImage}`);
|
onLoad={handleImageLoad}
|
||||||
if (bannerImage !== metadata.banner) {
|
|
||||||
setBannerImage(metadata.banner || metadata.poster);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
|
|
@ -643,6 +726,14 @@ const styles = StyleSheet.create({
|
||||||
opacity: 0.9,
|
opacity: 0.9,
|
||||||
letterSpacing: 0.2
|
letterSpacing: 0.2
|
||||||
},
|
},
|
||||||
|
skeletonGradient: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default React.memo(HeroSection);
|
export default React.memo(HeroSection);
|
||||||
Loading…
Reference in a new issue