From ef1c34a9c0ce74b6fdf0b5a61200b495783162c9 Mon Sep 17 00:00:00 2001 From: tapframe Date: Thu, 23 Oct 2025 00:38:45 +0530 Subject: [PATCH] improved animations --- src/components/TabletStreamsLayout.tsx | 208 ++++++++++++++++++------- 1 file changed, 153 insertions(+), 55 deletions(-) diff --git a/src/components/TabletStreamsLayout.tsx b/src/components/TabletStreamsLayout.tsx index 0426361..5190492 100644 --- a/src/components/TabletStreamsLayout.tsx +++ b/src/components/TabletStreamsLayout.tsx @@ -1,4 +1,4 @@ -import React, { memo } from 'react'; +import React, { memo, useEffect, useState } from 'react'; import { View, Text, @@ -13,6 +13,13 @@ 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, + withTiming, + withDelay, + Easing +} from 'react-native-reanimated'; // Lazy-safe community blur import for Android let AndroidBlurView: any = null; @@ -30,7 +37,6 @@ import { RootStackNavigationProp } from '../navigation/AppNavigator'; import ProviderFilter from './ProviderFilter'; import PulsingChip from './PulsingChip'; import StreamCard from './StreamCard'; -import AnimatedImage from './AnimatedImage'; interface TabletStreamsLayoutProps { // Background and content props @@ -117,6 +123,89 @@ 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); + + // 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 + const backdropSource = episodeImage ? { uri: episodeImage } : + bannerImage ? { uri: bannerImage } : + metadata?.poster ? { uri: metadata.poster } : undefined; + + // Animate backdrop when it loads + useEffect(() => { + if (backdropSource?.uri && backdropLoaded) { + backdropOpacity.value = withTiming(1, { + duration: 800, + easing: Easing.out(Easing.cubic) + }); + backdropScale.value = withTiming(1, { + duration: 1000, + easing: Easing.out(Easing.cubic) + }); + + // Animate content panels with delay after backdrop starts loading + leftPanelOpacity.value = withDelay(300, withTiming(1, { + duration: 600, + easing: Easing.out(Easing.cubic) + })); + leftPanelTranslateX.value = withDelay(300, withTiming(0, { + duration: 600, + easing: Easing.out(Easing.cubic) + })); + + rightPanelOpacity.value = withDelay(500, withTiming(1, { + duration: 600, + easing: Easing.out(Easing.cubic) + })); + rightPanelTranslateX.value = withDelay(500, withTiming(0, { + duration: 600, + easing: Easing.out(Easing.cubic) + })); + } + }, [backdropSource?.uri, backdropLoaded]); + + // Reset animation when source changes + useEffect(() => { + if (backdropSource?.uri) { + backdropOpacity.value = 0; + backdropScale.value = 1.05; + leftPanelOpacity.value = 0; + leftPanelTranslateX.value = -30; + rightPanelOpacity.value = 0; + rightPanelTranslateX.value = 30; + setBackdropLoaded(false); + } + }, [backdropSource?.uri]); + + // 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 renderStreamContent = () => { if (showNoSourcesError) { @@ -236,12 +325,15 @@ const TabletStreamsLayout: React.FC = ({ return ( - {/* Full Screen Background */} - + {/* Full Screen Background with Entrance Animation */} + + + = ({ /> {/* Left Panel: Movie Logo/Episode Info */} - + {type === 'movie' && metadata && ( {metadata.logo && !movieLogoError ? ( @@ -274,61 +366,63 @@ const TabletStreamsLayout: React.FC = ({ )} )} - + {/* Right Panel: Streams List */} - + {Platform.OS === 'android' && AndroidBlurView ? ( - - - {/* Always show filter container to prevent layout shift */} - - {!streamsEmpty && ( - - )} - - - {/* Active Scrapers Status */} - {activeFetchingScrapers.length > 0 && ( - - Fetching from: - - {activeFetchingScrapers.map((scraperName, index) => ( - - ))} - + + + + {/* Always show filter container to prevent layout shift */} + + {!streamsEmpty && ( + + )} - )} - {/* Stream content area - always show ScrollView to prevent flash */} - - {/* Show autoplay loading overlay if waiting for autoplay */} - {isAutoplayWaiting && !autoplayTriggered && ( - - - - Starting best stream... + {/* Active Scrapers Status */} + {activeFetchingScrapers.length > 0 && ( + + Fetching from: + + {activeFetchingScrapers.map((scraperName, index) => ( + + ))} )} - {renderStreamContent()} + {/* Stream content area - always show ScrollView to prevent flash */} + + {/* Show autoplay loading overlay if waiting for autoplay */} + {isAutoplayWaiting && !autoplayTriggered && ( + + + + Starting best stream... + + + )} + + {renderStreamContent()} + - - + + ) : ( = ({ )} - + ); }; @@ -619,6 +713,10 @@ const createStyles = (colors: any) => StyleSheet.create({ padding: 16, backgroundColor: 'transparent', }, + androidBlurView: { + flex: 1, + backgroundColor: 'transparent', + }, }); export default memo(TabletStreamsLayout);