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-reanimated": "~3.16.1",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
|
"react-native-shared-element": "^0.8.9",
|
||||||
"react-native-svg": "^15.11.2",
|
"react-native-svg": "^15.11.2",
|
||||||
"react-native-tab-view": "^4.0.10",
|
"react-native-tab-view": "^4.0.10",
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
|
|
@ -69,6 +70,7 @@
|
||||||
"react-native-vlc-media-player": "^1.0.87",
|
"react-native-vlc-media-player": "^1.0.87",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"react-native-wheel-color-picker": "^1.3.1",
|
"react-native-wheel-color-picker": "^1.3.1",
|
||||||
|
"react-navigation-shared-element": "^3.1.3",
|
||||||
"subsrt": "^1.1.1"
|
"subsrt": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
@ -12591,6 +12593,12 @@
|
||||||
"react-native": "*"
|
"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": {
|
"node_modules/react-native-slider": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-slider/-/react-native-slider-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-slider/-/react-native-slider-0.11.0.tgz",
|
||||||
|
|
@ -12971,6 +12979,20 @@
|
||||||
"async-limiter": "~1.0.0"
|
"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": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.14.2",
|
"version": "0.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
"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-reanimated": "~3.16.1",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
|
"react-native-shared-element": "^0.8.9",
|
||||||
"react-native-svg": "^15.11.2",
|
"react-native-svg": "^15.11.2",
|
||||||
"react-native-tab-view": "^4.0.10",
|
"react-native-tab-view": "^4.0.10",
|
||||||
"react-native-url-polyfill": "^2.0.0",
|
"react-native-url-polyfill": "^2.0.0",
|
||||||
|
|
@ -70,6 +71,7 @@
|
||||||
"react-native-vlc-media-player": "^1.0.87",
|
"react-native-vlc-media-player": "^1.0.87",
|
||||||
"react-native-web": "~0.19.13",
|
"react-native-web": "~0.19.13",
|
||||||
"react-native-wheel-color-picker": "^1.3.1",
|
"react-native-wheel-color-picker": "^1.3.1",
|
||||||
|
"react-navigation-shared-element": "^3.1.3",
|
||||||
"subsrt": "^1.1.1"
|
"subsrt": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useCallback, useState, useEffect, useMemo } from 'react';
|
import React, { useCallback, useState, useEffect, useMemo, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
|
|
@ -56,9 +56,7 @@ const MetadataScreen: React.FC = () => {
|
||||||
|
|
||||||
// Optimized state management - reduced state variables
|
// Optimized state management - reduced state variables
|
||||||
const [isContentReady, setIsContentReady] = useState(false);
|
const [isContentReady, setIsContentReady] = useState(false);
|
||||||
const [showSkeleton, setShowSkeleton] = useState(true);
|
const transitionOpacity = useSharedValue(1);
|
||||||
const transitionOpacity = useSharedValue(0);
|
|
||||||
const skeletonOpacity = useSharedValue(1);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
metadata,
|
metadata,
|
||||||
|
|
@ -187,26 +185,14 @@ const MetadataScreen: React.FC = () => {
|
||||||
// Memoized derived values for performance
|
// Memoized derived values for performance
|
||||||
const isReady = useMemo(() => !loading && metadata && !metadataError, [loading, metadata, metadataError]);
|
const isReady = useMemo(() => !loading && metadata && !metadataError, [loading, metadata, metadataError]);
|
||||||
|
|
||||||
// Smooth skeleton to content transition
|
// Simple content ready state management
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isReady && !isContentReady) {
|
if (isReady) {
|
||||||
// Small delay to ensure skeleton is rendered before starting transition
|
setIsContentReady(true);
|
||||||
setTimeout(() => {
|
transitionOpacity.value = withTiming(1, { duration: 200 });
|
||||||
// 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);
|
|
||||||
} else if (!isReady && isContentReady) {
|
} else if (!isReady && isContentReady) {
|
||||||
setIsContentReady(false);
|
setIsContentReady(false);
|
||||||
setShowSkeleton(true);
|
|
||||||
transitionOpacity.value = 0;
|
transitionOpacity.value = 0;
|
||||||
skeletonOpacity.value = 1;
|
|
||||||
}
|
}
|
||||||
}, [isReady, isContentReady]);
|
}, [isReady, isContentReady]);
|
||||||
|
|
||||||
|
|
@ -257,10 +243,6 @@ const MetadataScreen: React.FC = () => {
|
||||||
opacity: transitionOpacity.value,
|
opacity: transitionOpacity.value,
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
const skeletonStyle = useAnimatedStyle(() => ({
|
|
||||||
opacity: skeletonOpacity.value,
|
|
||||||
}), []);
|
|
||||||
|
|
||||||
// Memoized error component for performance
|
// Memoized error component for performance
|
||||||
const ErrorComponent = useMemo(() => {
|
const ErrorComponent = useMemo(() => {
|
||||||
if (!metadataError) return null;
|
if (!metadataError) return null;
|
||||||
|
|
@ -300,104 +282,124 @@ const MetadataScreen: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={StyleSheet.absoluteFill}>
|
<SafeAreaView
|
||||||
{/* Skeleton Loading Screen - with fade out transition */}
|
style={[containerStyle, styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
|
||||||
{showSkeleton && (
|
edges={['bottom']}
|
||||||
<Animated.View
|
>
|
||||||
style={[StyleSheet.absoluteFill, skeletonStyle]}
|
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" animated />
|
||||||
pointerEvents={metadata ? 'none' : 'auto'}
|
|
||||||
>
|
|
||||||
<MetadataLoadingScreen type={metadata?.type === 'movie' ? 'movie' : 'series'} />
|
|
||||||
</Animated.View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Main Content - with fade in transition */}
|
|
||||||
{metadata && (
|
{metadata && (
|
||||||
<Animated.View
|
<>
|
||||||
style={[StyleSheet.absoluteFill, transitionStyle]}
|
{/* Floating Header - Optimized */}
|
||||||
pointerEvents={metadata ? 'auto' : 'none'}
|
<FloatingHeader
|
||||||
>
|
metadata={metadata}
|
||||||
<SafeAreaView
|
logoLoadError={assetData.logoLoadError}
|
||||||
style={[containerStyle, styles.container, { backgroundColor: currentTheme.colors.darkBackground }]}
|
handleBack={handleBack}
|
||||||
edges={['bottom']}
|
handleToggleLibrary={handleToggleLibrary}
|
||||||
>
|
headerElementsY={animations.headerElementsY}
|
||||||
<StatusBar translucent backgroundColor="transparent" barStyle="light-content" animated />
|
inLibrary={inLibrary}
|
||||||
|
headerOpacity={animations.headerOpacity}
|
||||||
|
headerElementsOpacity={animations.headerElementsOpacity}
|
||||||
|
safeAreaTop={safeAreaTop}
|
||||||
|
setLogoLoadError={assetData.setLogoLoadError}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Floating Header - Optimized */}
|
<Animated.ScrollView
|
||||||
<FloatingHeader
|
style={styles.scrollView}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
onScroll={animations.scrollHandler}
|
||||||
|
scrollEventThrottle={16}
|
||||||
|
bounces={false}
|
||||||
|
overScrollMode="never"
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
>
|
||||||
|
{/* Hero Section - Optimized */}
|
||||||
|
<HeroSection
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
|
bannerImage={assetData.bannerImage}
|
||||||
|
loadingBanner={assetData.loadingBanner}
|
||||||
logoLoadError={assetData.logoLoadError}
|
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}
|
handleToggleLibrary={handleToggleLibrary}
|
||||||
headerElementsY={animations.headerElementsY}
|
|
||||||
inLibrary={inLibrary}
|
inLibrary={inLibrary}
|
||||||
headerOpacity={animations.headerOpacity}
|
id={id}
|
||||||
headerElementsOpacity={animations.headerElementsOpacity}
|
navigation={navigation}
|
||||||
safeAreaTop={safeAreaTop}
|
getPlayButtonText={watchProgressData.getPlayButtonText}
|
||||||
|
setBannerImage={assetData.setBannerImage}
|
||||||
setLogoLoadError={assetData.setLogoLoadError}
|
setLogoLoadError={assetData.setLogoLoadError}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Animated.ScrollView
|
{/* Main Content - Optimized */}
|
||||||
style={styles.scrollView}
|
<Animated.View style={contentStyle}>
|
||||||
showsVerticalScrollIndicator={false}
|
<MetadataDetails
|
||||||
onScroll={animations.scrollHandler}
|
|
||||||
scrollEventThrottle={16}
|
|
||||||
bounces={false}
|
|
||||||
overScrollMode="never"
|
|
||||||
contentContainerStyle={styles.scrollContent}
|
|
||||||
>
|
|
||||||
{/* Hero Section - Optimized */}
|
|
||||||
<HeroSection
|
|
||||||
metadata={metadata}
|
metadata={metadata}
|
||||||
bannerImage={assetData.bannerImage}
|
imdbId={imdbId}
|
||||||
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}
|
|
||||||
type={type as 'movie' | 'series'}
|
type={type as 'movie' | 'series'}
|
||||||
getEpisodeDetails={watchProgressData.getEpisodeDetails}
|
renderRatings={() => imdbId ? (
|
||||||
handleShowStreams={handleShowStreams}
|
<RatingsSection imdbId={imdbId} type={type === 'series' ? 'show' : 'movie'} />
|
||||||
handleToggleLibrary={handleToggleLibrary}
|
) : null}
|
||||||
inLibrary={inLibrary}
|
|
||||||
id={id}
|
|
||||||
navigation={navigation}
|
|
||||||
getPlayButtonText={watchProgressData.getPlayButtonText}
|
|
||||||
setBannerImage={assetData.setBannerImage}
|
|
||||||
setLogoLoadError={assetData.setLogoLoadError}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main Content - Optimized */}
|
{/* Cast Section with skeleton when loading */}
|
||||||
<Animated.View style={contentStyle}>
|
{loadingCast ? (
|
||||||
<MetadataDetails
|
<View style={styles.skeletonSection}>
|
||||||
metadata={metadata}
|
<View style={[styles.skeletonTitle, { backgroundColor: currentTheme.colors.elevation1 }]} />
|
||||||
imdbId={imdbId}
|
<View style={styles.skeletonCastRow}>
|
||||||
type={type as 'movie' | 'series'}
|
{[...Array(4)].map((_, index) => (
|
||||||
renderRatings={() => imdbId ? (
|
<View key={index} style={[styles.skeletonCastItem, { backgroundColor: currentTheme.colors.elevation1 }]} />
|
||||||
<RatingsSection imdbId={imdbId} type={type === 'series' ? 'show' : 'movie'} />
|
))}
|
||||||
) : null}
|
</View>
|
||||||
/>
|
</View>
|
||||||
|
) : (
|
||||||
<CastSection
|
<CastSection
|
||||||
cast={cast}
|
cast={cast}
|
||||||
loadingCast={loadingCast}
|
loadingCast={loadingCast}
|
||||||
onSelectCastMember={handleSelectCastMember}
|
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
|
<MoreLikeThisSection
|
||||||
recommendations={recommendations}
|
recommendations={recommendations}
|
||||||
loadingRecommendations={loadingRecommendations}
|
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
|
<SeriesContent
|
||||||
episodes={Object.values(groupedEpisodes).flat()}
|
episodes={Object.values(groupedEpisodes).flat()}
|
||||||
selectedSeason={selectedSeason}
|
selectedSeason={selectedSeason}
|
||||||
|
|
@ -407,15 +409,15 @@ const MetadataScreen: React.FC = () => {
|
||||||
groupedEpisodes={groupedEpisodes}
|
groupedEpisodes={groupedEpisodes}
|
||||||
metadata={metadata || undefined}
|
metadata={metadata || undefined}
|
||||||
/>
|
/>
|
||||||
) : (
|
)
|
||||||
metadata && <MovieContent metadata={metadata} />
|
) : (
|
||||||
)}
|
metadata && <MovieContent metadata={metadata} />
|
||||||
</Animated.View>
|
)}
|
||||||
</Animated.ScrollView>
|
</Animated.View>
|
||||||
</SafeAreaView>
|
</Animated.ScrollView>
|
||||||
</Animated.View>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -466,6 +468,44 @@ const styles = StyleSheet.create({
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: '600',
|
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;
|
export default MetadataScreen;
|
||||||
Loading…
Reference in a new issue