From 2d6b4afa2de9ef034330f88ba003616ff1de0341 Mon Sep 17 00:00:00 2001 From: tapframe Date: Tue, 6 Jan 2026 00:12:00 +0530 Subject: [PATCH] fix: added timeout for tabletstreamscreen to prevent blackscreen until backdrop is fetched --- src/components/TabletStreamsLayout.tsx | 95 +++++++++++++++++--------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/src/components/TabletStreamsLayout.tsx b/src/components/TabletStreamsLayout.tsx index 6a05b277..85c21a3c 100644 --- a/src/components/TabletStreamsLayout.tsx +++ b/src/components/TabletStreamsLayout.tsx @@ -12,9 +12,9 @@ import { LinearGradient } from 'expo-linear-gradient'; import FastImage from '@d11/react-native-fast-image'; import { MaterialIcons } from '@expo/vector-icons'; import { BlurView as ExpoBlurView } from 'expo-blur'; -import Animated, { - useSharedValue, - useAnimatedStyle, +import Animated, { + useSharedValue, + useAnimatedStyle, withTiming, withDelay, Easing @@ -44,36 +44,36 @@ interface TabletStreamsLayoutProps { metadata?: any; type: string; currentEpisode?: any; - + // Movie logo props movieLogoError: boolean; setMovieLogoError: (error: boolean) => void; - + // Stream-related props streamsEmpty: boolean; selectedProvider: string; filterItems: Array<{ id: string; name: string; }>; handleProviderChange: (provider: string) => void; activeFetchingScrapers: string[]; - + // Loading states isAutoplayWaiting: boolean; autoplayTriggered: boolean; showNoSourcesError: boolean; showInitialLoading: boolean; showStillFetching: boolean; - + // Stream rendering props sections: Array<{ title: string; addonId: string; data: Stream[]; isEmptyDueToQualityFilter?: boolean } | null>; renderSectionHeader: ({ section }: { section: { title: string; addonId: string; isEmptyDueToQualityFilter?: boolean } }) => React.ReactElement; handleStreamPress: (stream: Stream) => void; openAlert: (title: string, message: string) => void; - + // Settings and theme settings: any; currentTheme: any; colors: any; - + // Other props navigation: RootStackNavigationProp; insets: any; @@ -122,19 +122,19 @@ const TabletStreamsLayout: React.FC = ({ hasStremioStreamProviders, }) => { const styles = React.useMemo(() => createStyles(colors), [colors]); - + // Animation values for backdrop entrance const backdropOpacity = useSharedValue(0); const backdropScale = useSharedValue(1.05); const [backdropLoaded, setBackdropLoaded] = useState(false); const [backdropError, setBackdropError] = useState(false); - + // Animation values for content panels const leftPanelOpacity = useSharedValue(0); const leftPanelTranslateX = useSharedValue(-30); const rightPanelOpacity = useSharedValue(0); const rightPanelTranslateX = useSharedValue(30); - + // Get the backdrop source - prioritize episode thumbnail, then show backdrop, then poster // For episodes without thumbnails, use show's backdrop instead of poster const backdropSource = React.useMemo(() => { @@ -148,7 +148,7 @@ const TabletStreamsLayout: React.FC = ({ backdropError }); } - + // If episodeImage failed to load, skip it and use backdrop if (backdropError && episodeImage && episodeImage !== metadata?.poster) { if (__DEV__) console.log('[TabletStreamsLayout] Episode thumbnail failed, falling back to backdrop'); @@ -157,26 +157,55 @@ const TabletStreamsLayout: React.FC = ({ return { uri: bannerImage }; } } - + // If episodeImage exists and is not the same as poster, use it (real episode thumbnail) if (episodeImage && episodeImage !== metadata?.poster && !backdropError) { if (__DEV__) console.log('[TabletStreamsLayout] Using episode thumbnail:', episodeImage); return { uri: episodeImage }; } - + // If episodeImage is the same as poster (fallback case), prioritize backdrop if (bannerImage) { if (__DEV__) console.log('[TabletStreamsLayout] Using show backdrop:', bannerImage); return { uri: bannerImage }; } - + // No fallback to poster images - + if (__DEV__) console.log('[TabletStreamsLayout] No backdrop source found'); return undefined; }, [episodeImage, bannerImage, metadata?.poster, backdropError]); - - // Animate backdrop when it loads, or animate content immediately if no backdrop + + + useEffect(() => { + if (backdropSource?.uri && !backdropLoaded && !backdropError) { + + const timeoutId = setTimeout(() => { + + leftPanelOpacity.value = withTiming(1, { + duration: 600, + easing: Easing.out(Easing.cubic) + }); + leftPanelTranslateX.value = withTiming(0, { + duration: 600, + easing: Easing.out(Easing.cubic) + }); + + rightPanelOpacity.value = withDelay(200, withTiming(1, { + duration: 600, + easing: Easing.out(Easing.cubic) + })); + rightPanelTranslateX.value = withDelay(200, withTiming(0, { + duration: 600, + easing: Easing.out(Easing.cubic) + })); + }, 1000); + + return () => clearTimeout(timeoutId); + } + }, [backdropSource?.uri, backdropLoaded, backdropError]); + + useEffect(() => { if (backdropSource?.uri && backdropLoaded) { // Animate backdrop first @@ -188,7 +217,7 @@ const TabletStreamsLayout: React.FC = ({ duration: 1000, easing: Easing.out(Easing.cubic) }); - + // Animate content panels with delay after backdrop starts loading leftPanelOpacity.value = withDelay(300, withTiming(1, { duration: 600, @@ -198,7 +227,7 @@ const TabletStreamsLayout: React.FC = ({ duration: 600, easing: Easing.out(Easing.cubic) })); - + rightPanelOpacity.value = withDelay(500, withTiming(1, { duration: 600, easing: Easing.out(Easing.cubic) @@ -217,7 +246,7 @@ const TabletStreamsLayout: React.FC = ({ duration: 600, easing: Easing.out(Easing.cubic) }); - + rightPanelOpacity.value = withDelay(200, withTiming(1, { duration: 600, easing: Easing.out(Easing.cubic) @@ -228,7 +257,7 @@ const TabletStreamsLayout: React.FC = ({ })); } }, [backdropSource?.uri, backdropLoaded, backdropError]); - + // Reset animation when episode changes useEffect(() => { backdropOpacity.value = 0; @@ -240,28 +269,28 @@ const TabletStreamsLayout: React.FC = ({ setBackdropLoaded(false); setBackdropError(false); }, [episodeImage]); - + // Animated styles for backdrop const backdropAnimatedStyle = useAnimatedStyle(() => ({ opacity: backdropOpacity.value, transform: [{ scale: backdropScale.value }], })); - + // Animated styles for content panels const leftPanelAnimatedStyle = useAnimatedStyle(() => ({ opacity: leftPanelOpacity.value, transform: [{ translateX: leftPanelTranslateX.value }], })); - + const rightPanelAnimatedStyle = useAnimatedStyle(() => ({ opacity: rightPanelOpacity.value, transform: [{ translateX: rightPanelTranslateX.value }], })); - + const handleBackdropLoad = () => { setBackdropLoaded(true); }; - + const handleBackdropError = () => { if (__DEV__) console.log('[TabletStreamsLayout] Backdrop image failed to load:', backdropSource?.uri); setBackdropError(true); @@ -294,8 +323,8 @@ const TabletStreamsLayout: React.FC = ({ {isAutoplayWaiting ? 'Finding best stream for autoplay...' : - showStillFetching ? 'Still fetching streams…' : - 'Finding available streams...'} + showStillFetching ? 'Still fetching streams…' : + 'Finding available streams...'} ); @@ -311,7 +340,7 @@ const TabletStreamsLayout: React.FC = ({ // Flatten sections into a single list with header items type ListItem = { type: 'header'; title: string; addonId: string } | { type: 'stream'; stream: Stream; index: number }; - + const flatListData: ListItem[] = []; sections .filter(Boolean) @@ -327,7 +356,7 @@ const TabletStreamsLayout: React.FC = ({ if (item.type === 'header') { return renderSectionHeader({ section: { title: item.title, addonId: item.addonId } }); } - + const stream = item.stream; return ( = ({ locations={[0, 0.5, 1]} style={styles.tabletFullScreenGradient} /> - + {/* Left Panel: Movie Logo/Episode Info */} {type === 'movie' && metadata ? (