mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-03-11 17:45:38 +00:00
Add react-native-shared-element and react-navigation-shared-element dependencies; refactor MetadataScreen for optimized loading and transitions
This commit is contained in:
parent
14dd507d50
commit
faef964d46
3 changed files with 170 additions and 106 deletions
22
package-lock.json
generated
22
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Reference in a new issue