Add react-native-shared-element and react-navigation-shared-element dependencies; refactor MetadataScreen for optimized loading and transitions

This commit is contained in:
tapframe 2025-06-25 00:43:30 +05:30
parent 14dd507d50
commit faef964d46
3 changed files with 170 additions and 106 deletions

22
package-lock.json generated
View file

@ -62,6 +62,7 @@
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-shared-element": "^0.8.9",
"react-native-svg": "^15.11.2",
"react-native-tab-view": "^4.0.10",
"react-native-url-polyfill": "^2.0.0",
@ -69,6 +70,7 @@
"react-native-vlc-media-player": "^1.0.87",
"react-native-web": "~0.19.13",
"react-native-wheel-color-picker": "^1.3.1",
"react-navigation-shared-element": "^3.1.3",
"subsrt": "^1.1.1"
},
"devDependencies": {
@ -12591,6 +12593,12 @@
"react-native": "*"
}
},
"node_modules/react-native-shared-element": {
"version": "0.8.9",
"resolved": "https://registry.npmjs.org/react-native-shared-element/-/react-native-shared-element-0.8.9.tgz",
"integrity": "sha512-vlzhv3amkJm+8gA0WSeLzcCKNtN/ypZbic3IZ4Bwwr6GeWDrYzZ6k7PdHCioy7fwIVOJ1X9Pi/aYF9HK4Kb0qg==",
"license": "MIT"
},
"node_modules/react-native-slider": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/react-native-slider/-/react-native-slider-0.11.0.tgz",
@ -12971,6 +12979,20 @@
"async-limiter": "~1.0.0"
}
},
"node_modules/react-navigation-shared-element": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/react-navigation-shared-element/-/react-navigation-shared-element-3.1.3.tgz",
"integrity": "sha512-U1BZp7dEdcTNHggfkq3WEBlJeg4HwFhFdj7a0i0Uql/7mg2IHQg/bZaqM2jQvJITkABge6Hz5fZixIF8jyzpkg==",
"license": "MIT",
"dependencies": {
"hoist-non-react-statics": "^3.3.2"
},
"peerDependencies": {
"react": "*",
"react-native": "*",
"react-native-shared-element": "*"
}
},
"node_modules/react-refresh": {
"version": "0.14.2",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",

View file

@ -63,6 +63,7 @@
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-shared-element": "^0.8.9",
"react-native-svg": "^15.11.2",
"react-native-tab-view": "^4.0.10",
"react-native-url-polyfill": "^2.0.0",
@ -70,6 +71,7 @@
"react-native-vlc-media-player": "^1.0.87",
"react-native-web": "~0.19.13",
"react-native-wheel-color-picker": "^1.3.1",
"react-navigation-shared-element": "^3.1.3",
"subsrt": "^1.1.1"
},
"devDependencies": {

View file

@ -1,4 +1,4 @@
import React, { useCallback, useState, useEffect, useMemo } from 'react';
import React, { useCallback, useState, useEffect, useMemo, useRef } from 'react';
import {
View,
Text,
@ -56,9 +56,7 @@ const MetadataScreen: React.FC = () => {
// Optimized state management - reduced state variables
const [isContentReady, setIsContentReady] = useState(false);
const [showSkeleton, setShowSkeleton] = useState(true);
const transitionOpacity = useSharedValue(0);
const skeletonOpacity = useSharedValue(1);
const transitionOpacity = useSharedValue(1);
const {
metadata,
@ -187,26 +185,14 @@ const MetadataScreen: React.FC = () => {
// Memoized derived values for performance
const isReady = useMemo(() => !loading && metadata && !metadataError, [loading, metadata, metadataError]);
// Smooth skeleton to content transition
// Simple content ready state management
useEffect(() => {
if (isReady && !isContentReady) {
// Small delay to ensure skeleton is rendered before starting transition
setTimeout(() => {
// Start fade out skeleton and fade in content simultaneously
skeletonOpacity.value = withTiming(0, { duration: 300 });
transitionOpacity.value = withTiming(1, { duration: 400 });
// Hide skeleton after fade out completes
setTimeout(() => {
setShowSkeleton(false);
setIsContentReady(true);
}, 300);
}, 100);
if (isReady) {
setIsContentReady(true);
transitionOpacity.value = withTiming(1, { duration: 200 });
} else if (!isReady && isContentReady) {
setIsContentReady(false);
setShowSkeleton(true);
transitionOpacity.value = 0;
skeletonOpacity.value = 1;
}
}, [isReady, isContentReady]);
@ -257,10 +243,6 @@ const MetadataScreen: React.FC = () => {
opacity: transitionOpacity.value,
}), []);
const skeletonStyle = useAnimatedStyle(() => ({
opacity: skeletonOpacity.value,
}), []);
// Memoized error component for performance
const ErrorComponent = useMemo(() => {
if (!metadataError) return null;
@ -300,104 +282,124 @@ const MetadataScreen: React.FC = () => {
}
return (
<View style={StyleSheet.absoluteFill}>
{/* Skeleton Loading Screen - with fade out transition */}
{showSkeleton && (
<Animated.View
style={[StyleSheet.absoluteFill, skeletonStyle]}
pointerEvents={metadata ? 'none' : 'auto'}
>
<MetadataLoadingScreen type={metadata?.type === 'movie' ? 'movie' : 'series'} />
</Animated.View>
)}
{/* Main Content - with fade in transition */}
<SafeAreaView
style={[containerStyle, styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
edges={['bottom']}
>
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" animated />
{metadata && (
<Animated.View
style={[StyleSheet.absoluteFill, transitionStyle]}
pointerEvents={metadata ? 'auto' : 'none'}
>
<SafeAreaView
style={[containerStyle, styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
edges={['bottom']}
<>
{/* Floating Header - Optimized */}
<FloatingHeader
metadata={metadata}
logoLoadError={assetData.logoLoadError}
handleBack={handleBack}
handleToggleLibrary={handleToggleLibrary}
headerElementsY={animations.headerElementsY}
inLibrary={inLibrary}
headerOpacity={animations.headerOpacity}
headerElementsOpacity={animations.headerElementsOpacity}
safeAreaTop={safeAreaTop}
setLogoLoadError={assetData.setLogoLoadError}
/>
<Animated.ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
onScroll={animations.scrollHandler}
scrollEventThrottle={16}
bounces={false}
overScrollMode="never"
contentContainerStyle={styles.scrollContent}
>
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" animated />
{/* Floating Header - Optimized */}
<FloatingHeader
{/* Hero Section - Optimized */}
<HeroSection
metadata={metadata}
bannerImage={assetData.bannerImage}
loadingBanner={assetData.loadingBanner}
logoLoadError={assetData.logoLoadError}
handleBack={handleBack}
scrollY={animations.scrollY}
heroHeight={animations.heroHeight}
heroOpacity={animations.heroOpacity}
logoOpacity={animations.logoOpacity}
buttonsOpacity={animations.buttonsOpacity}
buttonsTranslateY={animations.buttonsTranslateY}
watchProgressOpacity={animations.watchProgressOpacity}
watchProgressWidth={animations.watchProgressWidth}
watchProgress={watchProgressData.watchProgress}
type={type as 'movie' | 'series'}
getEpisodeDetails={watchProgressData.getEpisodeDetails}
handleShowStreams={handleShowStreams}
handleToggleLibrary={handleToggleLibrary}
headerElementsY={animations.headerElementsY}
inLibrary={inLibrary}
headerOpacity={animations.headerOpacity}
headerElementsOpacity={animations.headerElementsOpacity}
safeAreaTop={safeAreaTop}
id={id}
navigation={navigation}
getPlayButtonText={watchProgressData.getPlayButtonText}
setBannerImage={assetData.setBannerImage}
setLogoLoadError={assetData.setLogoLoadError}
/>
<Animated.ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
onScroll={animations.scrollHandler}
scrollEventThrottle={16}
bounces={false}
overScrollMode="never"
contentContainerStyle={styles.scrollContent}
>
{/* Hero Section - Optimized */}
<HeroSection
{/* Main Content - Optimized */}
<Animated.View style={contentStyle}>
<MetadataDetails
metadata={metadata}
bannerImage={assetData.bannerImage}
loadingBanner={assetData.loadingBanner}
logoLoadError={assetData.logoLoadError}
scrollY={animations.scrollY}
heroHeight={animations.heroHeight}
heroOpacity={animations.heroOpacity}
logoOpacity={animations.logoOpacity}
buttonsOpacity={animations.buttonsOpacity}
buttonsTranslateY={animations.buttonsTranslateY}
watchProgressOpacity={animations.watchProgressOpacity}
watchProgressWidth={animations.watchProgressWidth}
watchProgress={watchProgressData.watchProgress}
imdbId={imdbId}
type={type as 'movie' | 'series'}
getEpisodeDetails={watchProgressData.getEpisodeDetails}
handleShowStreams={handleShowStreams}
handleToggleLibrary={handleToggleLibrary}
inLibrary={inLibrary}
id={id}
navigation={navigation}
getPlayButtonText={watchProgressData.getPlayButtonText}
setBannerImage={assetData.setBannerImage}
setLogoLoadError={assetData.setLogoLoadError}
renderRatings={() => imdbId ? (
<RatingsSection imdbId={imdbId} type={type === 'series' ? 'show' : 'movie'} />
) : null}
/>
{/* Main Content - Optimized */}
<Animated.View style={contentStyle}>
<MetadataDetails
metadata={metadata}
imdbId={imdbId}
type={type as 'movie' | 'series'}
renderRatings={() => imdbId ? (
<RatingsSection imdbId={imdbId} type={type === 'series' ? 'show' : 'movie'} />
) : null}
/>
{/* Cast Section with skeleton when loading */}
{loadingCast ? (
<View style={styles.skeletonSection}>
<View style={[styles.skeletonTitle, { backgroundColor: currentTheme.colors.elevation1 }]} />
<View style={styles.skeletonCastRow}>
{[...Array(4)].map((_, index) => (
<View key={index} style={[styles.skeletonCastItem, { backgroundColor: currentTheme.colors.elevation1 }]} />
))}
</View>
</View>
) : (
<CastSection
cast={cast}
loadingCast={loadingCast}
onSelectCastMember={handleSelectCastMember}
/>
)}
{type === 'movie' && (
{/* Recommendations Section with skeleton when loading */}
{type === 'movie' && (
loadingRecommendations ? (
<View style={styles.skeletonSection}>
<View style={[styles.skeletonTitle, { backgroundColor: currentTheme.colors.elevation1 }]} />
<View style={styles.skeletonRecommendationsRow}>
{[...Array(3)].map((_, index) => (
<View key={index} style={[styles.skeletonRecommendationItem, { backgroundColor: currentTheme.colors.elevation1 }]} />
))}
</View>
</View>
) : (
<MoreLikeThisSection
recommendations={recommendations}
loadingRecommendations={loadingRecommendations}
/>
)}
)
)}
{type === 'series' ? (
{/* Series/Movie Content with episode skeleton when loading */}
{type === 'series' ? (
(loadingSeasons || Object.keys(groupedEpisodes).length === 0) ? (
<View style={styles.skeletonSection}>
<View style={[styles.skeletonTitle, { backgroundColor: currentTheme.colors.elevation1 }]} />
<View style={styles.skeletonEpisodesContainer}>
{[...Array(6)].map((_, index) => (
<View key={index} style={[styles.skeletonEpisodeItem, { backgroundColor: currentTheme.colors.elevation1 }]} />
))}
</View>
</View>
) : (
<SeriesContent
episodes={Object.values(groupedEpisodes).flat()}
selectedSeason={selectedSeason}
@ -407,15 +409,15 @@ const MetadataScreen: React.FC = () => {
groupedEpisodes={groupedEpisodes}
metadata={metadata || undefined}
/>
) : (
metadata && <MovieContent metadata={metadata} />
)}
</Animated.View>
</Animated.ScrollView>
</SafeAreaView>
</Animated.View>
)
) : (
metadata && <MovieContent metadata={metadata} />
)}
</Animated.View>
</Animated.ScrollView>
</>
)}
</View>
</SafeAreaView>
);
};
@ -466,6 +468,44 @@ const styles = StyleSheet.create({
fontSize: 16,
fontWeight: '600',
},
// Skeleton loading styles
skeletonSection: {
padding: 16,
marginBottom: 24,
},
skeletonTitle: {
width: 150,
height: 20,
borderRadius: 4,
marginBottom: 16,
},
skeletonCastRow: {
flexDirection: 'row',
gap: 12,
},
skeletonCastItem: {
width: 80,
height: 120,
borderRadius: 8,
},
skeletonRecommendationsRow: {
flexDirection: 'row',
gap: 12,
},
skeletonRecommendationItem: {
width: 120,
height: 180,
borderRadius: 8,
},
skeletonEpisodesContainer: {
gap: 12,
},
skeletonEpisodeItem: {
width: '100%',
height: 80,
borderRadius: 8,
marginBottom: 8,
},
});
export default MetadataScreen;