From 19420e901e514bec0c539a7242e8c891e14b1ac2 Mon Sep 17 00:00:00 2001 From: tapframe Date: Wed, 5 Nov 2025 14:48:21 +0530 Subject: [PATCH] revert legacy ui --- ios/Nuvio.xcodeproj/project.pbxproj | 6 +- src/components/home/FeaturedContent.tsx | 1115 ++++++++++------------- src/screens/HomeScreen.tsx | 4 +- 3 files changed, 509 insertions(+), 616 deletions(-) diff --git a/ios/Nuvio.xcodeproj/project.pbxproj b/ios/Nuvio.xcodeproj/project.pbxproj index 64b7e1c..3acee97 100644 --- a/ios/Nuvio.xcodeproj/project.pbxproj +++ b/ios/Nuvio.xcodeproj/project.pbxproj @@ -459,7 +459,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; - PRODUCT_NAME = "Nuvio"; + PRODUCT_NAME = Nuvio; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -490,8 +490,8 @@ "-lc++", ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; - PRODUCT_BUNDLE_IDENTIFIER = "com.nuvio.app"; - PRODUCT_NAME = "Nuvio"; + PRODUCT_BUNDLE_IDENTIFIER = com.nuvio.app; + PRODUCT_NAME = Nuvio; SWIFT_OBJC_BRIDGING_HEADER = "Nuvio/Nuvio-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/src/components/home/FeaturedContent.tsx b/src/components/home/FeaturedContent.tsx index 7030211..31d399c 100644 --- a/src/components/home/FeaturedContent.tsx +++ b/src/components/home/FeaturedContent.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef, useMemo, useCallback } from 'react'; +import React, { useState, useEffect, useRef, useMemo } from 'react'; import { View, Text, @@ -23,11 +23,8 @@ import Animated, { useSharedValue, withTiming, Easing, - withDelay, - interpolate, - Extrapolation + withDelay } from 'react-native-reanimated'; -import Carousel, { ICarouselInstance, Pagination } from 'react-native-reanimated-carousel'; import { StreamingContent } from '../../services/catalogService'; import { SkeletonFeatured } from './SkeletonLoaders'; import { hasValidLogoFormat, isTmdbUrl } from '../../utils/logoUtils'; @@ -35,9 +32,9 @@ import { logger } from '../../utils/logger'; import { useTheme } from '../../contexts/ThemeContext'; interface FeaturedContentProps { - featuredContent: StreamingContent[] | StreamingContent | null; - isSaved: (item: StreamingContent) => Promise; - handleSaveToLibrary: (item: StreamingContent) => void; + featuredContent: StreamingContent | null; + isSaved: boolean; + handleSaveToLibrary: () => void; loading?: boolean; onRetry?: () => void; } @@ -45,457 +42,6 @@ interface FeaturedContentProps { // Cache to store preloaded images const imageCache: Record = {}; -interface FeaturedContentItemProps { - item: StreamingContent; - isSaved: boolean; - onSaveToLibrary: () => void; - onPressInfo: () => void; - onPressPlay: () => void; -} - -// Individual item component for the carousel -const FeaturedContentItem = React.memo(({ item, isSaved, onSaveToLibrary, onPressInfo, onPressPlay }: FeaturedContentItemProps) => { - const { currentTheme } = useTheme(); - const [bannerUrl, setBannerUrl] = useState(null); - const [logoUrl, setLogoUrl] = useState(null); - const [logoLoaded, setLogoLoaded] = useState(false); - const [bannerLoaded, setBannerLoaded] = useState(false); - const [logoError, setLogoError] = useState(false); - const [bannerError, setBannerError] = useState(false); - const logoOpacity = useSharedValue(0); - const bannerOpacity = useSharedValue(0); - const posterOpacity = useSharedValue(0); - const prevContentIdRef = useRef(null); - const [logoLoadError, setLogoLoadError] = useState(false); - - // Enhanced poster transition animations - const posterScale = useSharedValue(1); - const posterTranslateY = useSharedValue(0); - const overlayOpacity = useSharedValue(0.15); - - // Animation values - const posterAnimatedStyle = useAnimatedStyle(() => ({ - opacity: posterOpacity.value, - transform: [ - { scale: posterScale.value }, - { translateY: posterTranslateY.value } - ], - })); - - const logoAnimatedStyle = useAnimatedStyle(() => ({ - opacity: logoOpacity.value, - })); - - const contentOpacity = useSharedValue(1); - const buttonsOpacity = useSharedValue(1); - - const contentAnimatedStyle = useAnimatedStyle(() => ({ - opacity: contentOpacity.value, - })); - - const buttonsAnimatedStyle = useAnimatedStyle(() => ({ - opacity: buttonsOpacity.value, - })); - - const overlayAnimatedStyle = useAnimatedStyle(() => ({ - opacity: overlayOpacity.value, - })); - - // Preload the image - const preloadImage = async (url: string): Promise => { - const t0 = nowMs(); - logger.debug('[FeaturedContentItem] preloadImage:start', { url }); - // Skip if already cached to prevent redundant prefetch - if (imageCache[url]) return true; - - try { - // Simplified validation to reduce CPU overhead - if (!url || typeof url !== 'string') return false; - - // Add timeout guard to prevent hanging preloads - const timeout = new Promise((_, reject) => { - const t = setTimeout(() => { - clearTimeout(t as any); - reject(new Error('preload-timeout')); - }, 1500); - }); - - // FastImage.preload doesn't return a promise, so we just call it and use timeout - FastImage.preload([{ uri: url }]); - await timeout; - imageCache[url] = true; - logger.debug('[FeaturedContentItem] preloadImage:success', { url, duration: since(t0) }); - return true; - } catch (error) { - // Clear any partial cache entry on error - delete imageCache[url]; - logger.warn('[FeaturedContentItem] preloadImage:error', { url, duration: since(t0), error: String(error) }); - return false; - } - }; - - // Reset logo error state when content changes - useEffect(() => { - setLogoLoadError(false); - }, [item?.id]); - - // Use logo from item data - useEffect(() => { - if (!item) { - setLogoUrl(null); - setLogoLoadError(false); - return; - } - - const logo = item.logo; - logger.info('[FeaturedContentItem] using logo from data', { - id: item.id, - name: item.name, - hasLogo: Boolean(logo), - logo: logo, - logoSource: logo ? (isTmdbUrl(logo) ? 'tmdb' : 'addon') : 'none', - type: item.type - }); - - setLogoUrl(logo || null); - setLogoLoadError(!logo); - setLogoError(false); - }, [item]); - - // Load poster and logo - useEffect(() => { - if (!item) return; - - const posterUrl = item.banner || item.poster; - const contentId = item.id; - const isContentChange = contentId !== prevContentIdRef.current; - const t0 = nowMs(); - logger.info('[FeaturedContentItem] content:update', { id: contentId, isContentChange, posterUrlExists: Boolean(posterUrl) }); - - // Enhanced content change detection and animations - if (isContentChange) { - // Animate out current content - if (prevContentIdRef.current) { - posterOpacity.value = withTiming(0, { - duration: 300, - easing: Easing.out(Easing.cubic) - }); - posterScale.value = withTiming(0.95, { - duration: 300, - easing: Easing.out(Easing.cubic) - }); - overlayOpacity.value = withTiming(0.6, { - duration: 300, - easing: Easing.out(Easing.cubic) - }); - contentOpacity.value = withTiming(0.3, { - duration: 200, - easing: Easing.out(Easing.cubic) - }); - buttonsOpacity.value = withTiming(0.3, { - duration: 200, - easing: Easing.out(Easing.cubic) - }); - } else { - // Initial load - start from 0 but don't animate if we're just mounting - posterOpacity.value = 0; - posterScale.value = 1.1; - overlayOpacity.value = 0; - contentOpacity.value = 0; - buttonsOpacity.value = 0; - } - logoOpacity.value = 0; - } - - prevContentIdRef.current = contentId; - - // Set poster URL for immediate display - if (posterUrl && posterUrl !== bannerUrl) { - setBannerUrl(posterUrl); - } - - // Load images with enhanced animations - const loadImages = async () => { - // Small delay to allow fade out animation to complete - await new Promise(resolve => setTimeout(resolve, isContentChange && prevContentIdRef.current ? 300 : 0)); - - // Load poster with enhanced transition - if (posterUrl) { - const tPoster = nowMs(); - const posterSuccess = await preloadImage(posterUrl); - if (posterSuccess) { - // Animate in new poster with scale and fade - posterScale.value = withTiming(1, { - duration: 800, - easing: Easing.out(Easing.cubic) - }); - posterOpacity.value = withTiming(1, { - duration: 700, - easing: Easing.out(Easing.cubic) - }); - overlayOpacity.value = withTiming(0.15, { - duration: 600, - easing: Easing.out(Easing.cubic) - }); - logger.debug('[FeaturedContentItem] poster:ready', { id: contentId, duration: since(tPoster) }); - - // Animate content back in with delay - contentOpacity.value = withDelay(200, withTiming(1, { - duration: 600, - easing: Easing.out(Easing.cubic) - })); - buttonsOpacity.value = withDelay(400, withTiming(1, { - duration: 500, - easing: Easing.out(Easing.cubic) - })); - } else { - // If preload fails, still show the image but without animation - posterOpacity.value = 1; - posterScale.value = 1; - overlayOpacity.value = 0.15; - contentOpacity.value = 1; - buttonsOpacity.value = 1; - } - } - - // Load logo if available with enhanced timing - if (logoUrl) { - const tLogo = nowMs(); - const logoSuccess = await preloadImage(logoUrl); - if (logoSuccess) { - logger.debug('[FeaturedContentItem] logo:preload:success', { id: contentId, duration: since(tLogo) }); - } else { - logger.debug('[FeaturedContentItem] logo:preload:failed', { id: contentId, duration: since(tLogo) }); - } - - // Always animate in the logo since we have the URL - logoOpacity.value = withDelay(500, withTiming(1, { - duration: 600, - easing: Easing.out(Easing.cubic) - })); - logger.debug('[FeaturedContentItem] logo:animated', { id: contentId }); - } - logger.info('[FeaturedContentItem] images:load:done', { id: contentId, total: since(t0) }); - }; - - loadImages(); - }, [item?.id, logoUrl]); - - const onLogoLoadError = () => { - setLogoLoaded(true); - setLogoLoadError(true); - logger.warn('[FeaturedContentItem] logo:onError', { id: item?.id, url: logoUrl }); - }; - - const isTablet = width >= 768; - - if (isTablet) { - return ( - - - - - - - - - - {logoUrl && !logoLoadError ? ( - - - - ) : ( - - {item.name} - - )} - - - {item.genres?.slice(0, 4).map((genre, index, array) => ( - - - {genre} - - {index < array.length - 1 && ( - - )} - - ))} - - - {item.description && ( - - {item.description} - - )} - - - - - - Play Now - - - - - - - {isSaved ? "Saved" : "My List"} - - - - - - - More Info - - - - - - ); - } else { - // Phone layout - return ( - - - - - - - {logoUrl && !logoLoadError ? ( - - - - ) : ( - - {item.name} - - )} - - {item.genres?.slice(0, 3).map((genre, index, array) => ( - - - {genre} - - {index < array.length - 1 && ( - - )} - - ))} - - - - - - - - {isSaved ? "Saved" : "Save"} - - - - - - - Play - - - - - - - Info - - - - - - - - ); - } -}); - const { width, height } = Dimensions.get('window'); // Utility to determine if device is tablet-sized @@ -593,22 +139,22 @@ const NoFeaturedContent = ({ onRetry }: { onRetry?: () => void }) => { }; const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loading, onRetry }: FeaturedContentProps) => { - // Normalize data to always be an array - const items = useMemo(() => { - if (Array.isArray(featuredContent)) { - return featuredContent; - } else if (featuredContent) { - return [featuredContent]; - } - return []; - }, [featuredContent]); - const navigation = useNavigation>(); const { currentTheme } = useTheme(); - const scrollOffsetValue = useSharedValue(0); - const progress = useSharedValue(0); - const carouselRef = useRef(null); + const [bannerUrl, setBannerUrl] = useState(null); + const [logoUrl, setLogoUrl] = useState(null); + const [logoLoaded, setLogoLoaded] = useState(false); + const [bannerLoaded, setBannerLoaded] = useState(false); + const [showSkeleton, setShowSkeleton] = useState(true); + const [logoError, setLogoError] = useState(false); + const [bannerError, setBannerError] = useState(false); + const logoOpacity = useSharedValue(0); + const bannerOpacity = useSharedValue(0); + const posterOpacity = useSharedValue(0); + const prevContentIdRef = useRef(null); + const [logoLoadError, setLogoLoadError] = useState(false); const firstRenderTsRef = useRef(nowMs()); + const lastContentChangeTsRef = useRef(0); // Initial diagnostics useEffect(() => { @@ -621,169 +167,516 @@ const FeaturedContent = ({ featuredContent, isSaved, handleSaveToLibrary, loadin }; }, []); - // Stable hero height for tablets to prevent layout jumps + // Enhanced poster transition animations + const posterScale = useSharedValue(1); + const posterTranslateY = useSharedValue(0); + const overlayOpacity = useSharedValue(0.15); + + // Animation values + const posterAnimatedStyle = useAnimatedStyle(() => ({ + opacity: posterOpacity.value, + transform: [ + { scale: posterScale.value }, + { translateY: posterTranslateY.value } + ], + })); + + const logoAnimatedStyle = useAnimatedStyle(() => ({ + opacity: logoOpacity.value, + })); + + const contentOpacity = useSharedValue(1); // Start visible + const buttonsOpacity = useSharedValue(1); + + const contentAnimatedStyle = useAnimatedStyle(() => ({ + opacity: contentOpacity.value, + })); + + const buttonsAnimatedStyle = useAnimatedStyle(() => ({ + opacity: buttonsOpacity.value, + })); + + const overlayAnimatedStyle = useAnimatedStyle(() => ({ + opacity: overlayOpacity.value, + })); + + // Stable hero height for tablets to prevent layout jumps; keep hooks unconditional const tabletHeroHeight = useMemo(() => { const aspectBased = width * 0.56; // ~16:9 visual const screenBased = height * 0.62; return Math.min(screenBased, aspectBased); - }, [width, height]); + }, [width, height, featuredContent?.id]); - // Wrapper component to handle async isSaved check - const CarouselItemWrapper = React.memo(({ item }: { item: StreamingContent }) => { - const [savedStatus, setSavedStatus] = useState(false); + // Preload the image + const preloadImage = async (url: string): Promise => { + const t0 = nowMs(); + logger.debug('[FeaturedContent] preloadImage:start', { url }); + // Skip if already cached to prevent redundant prefetch + if (imageCache[url]) return true; - useEffect(() => { - const checkSavedStatus = async () => { - try { - const status = await isSaved(item); - setSavedStatus(status); - } catch (error) { - logger.error('Error checking saved status:', error); - setSavedStatus(false); - } - }; - checkSavedStatus(); - }, [item.id]); + try { + // Simplified validation to reduce CPU overhead + if (!url || typeof url !== 'string') return false; - return ( - handleSaveToLibrary(item)} - onPressInfo={() => { - navigation.navigate('Metadata', { - id: item.id, - type: item.type - }); - }} - onPressPlay={() => { - navigation.navigate('Streams', { - id: item.id, - type: item.type - }); - }} - /> - ); - }); + // Add timeout guard to prevent hanging preloads + const timeout = new Promise((_, reject) => { + const t = setTimeout(() => { + clearTimeout(t as any); + reject(new Error('preload-timeout')); + }, 1500); + }); - // Render item function for the carousel - const renderItem = useCallback(({ item }: { item: StreamingContent }) => { - return ( - - - - ); - }, [isSaved, handleSaveToLibrary, navigation]); + // FastImage.preload doesn't return a promise, so we just call it and use timeout + FastImage.preload([{ uri: url }]); + await timeout; + imageCache[url] = true; + logger.debug('[FeaturedContent] preloadImage:success', { url, duration: since(t0) }); + return true; + } catch (error) { + // Clear any partial cache entry on error + delete imageCache[url]; + logger.warn('[FeaturedContent] preloadImage:error', { url, duration: since(t0), error: String(error) }); + return false; + } + }; - // Pagination press handler - const onPressPagination = useCallback((index: number) => { - carouselRef.current?.scrollTo({ - // Scroll to nearest target - count: index - progress.value, - animated: true, + // Reset logo error state when content changes + useEffect(() => { + setLogoLoadError(false); + }, [featuredContent?.id]); + + // Use logo from featuredContent data (already processed by useFeaturedContent hook) + useEffect(() => { + if (!featuredContent) { + setLogoUrl(null); + setLogoLoadError(false); + return; + } + + // Simply use the logo that's already been processed by the useFeaturedContent hook + const logo = featuredContent.logo; + logger.info('[FeaturedContent] using logo from data', { + id: featuredContent.id, + name: featuredContent.name, + hasLogo: Boolean(logo), + logo: logo, + logoSource: logo ? (isTmdbUrl(logo) ? 'tmdb' : 'addon') : 'none', + type: featuredContent.type }); - }, []); + + setLogoUrl(logo || null); + setLogoLoadError(!logo); + setLogoError(false); // Reset any previous errors + }, [featuredContent]); + + // Load poster and logo + useEffect(() => { + if (!featuredContent) return; + + const posterUrl = featuredContent.banner || featuredContent.poster; + const contentId = featuredContent.id; + const isContentChange = contentId !== prevContentIdRef.current; + const t0 = nowMs(); + logger.info('[FeaturedContent] content:update', { id: contentId, isContentChange, posterUrlExists: Boolean(posterUrl), sinceMount: since(firstRenderTsRef.current) }); + + // Enhanced content change detection and animations + if (isContentChange) { + // Animate out current content + if (prevContentIdRef.current) { + posterOpacity.value = withTiming(0, { + duration: 300, + easing: Easing.out(Easing.cubic) + }); + posterScale.value = withTiming(0.95, { + duration: 300, + easing: Easing.out(Easing.cubic) + }); + overlayOpacity.value = withTiming(0.6, { + duration: 300, + easing: Easing.out(Easing.cubic) + }); + contentOpacity.value = withTiming(0.3, { + duration: 200, + easing: Easing.out(Easing.cubic) + }); + buttonsOpacity.value = withTiming(0.3, { + duration: 200, + easing: Easing.out(Easing.cubic) + }); + } else { + // Initial load - start from 0 but don't animate if we're just mounting + posterOpacity.value = 0; + posterScale.value = 1.1; + overlayOpacity.value = 0; + contentOpacity.value = 0; + buttonsOpacity.value = 0; + } + logoOpacity.value = 0; + } + + prevContentIdRef.current = contentId; + + // Set poster URL for immediate display - only if it's different to prevent flash + if (posterUrl && posterUrl !== bannerUrl) { + setBannerUrl(posterUrl); + } + + // Load images with enhanced animations + const loadImages = async () => { + // Small delay to allow fade out animation to complete + await new Promise(resolve => setTimeout(resolve, isContentChange && prevContentIdRef.current ? 300 : 0)); + + // Load poster with enhanced transition + if (posterUrl) { + const tPoster = nowMs(); + const posterSuccess = await preloadImage(posterUrl); + if (posterSuccess) { + // Animate in new poster with scale and fade + posterScale.value = withTiming(1, { + duration: 800, + easing: Easing.out(Easing.cubic) + }); + posterOpacity.value = withTiming(1, { + duration: 700, + easing: Easing.out(Easing.cubic) + }); + overlayOpacity.value = withTiming(0.15, { + duration: 600, + easing: Easing.out(Easing.cubic) + }); + logger.debug('[FeaturedContent] poster:ready', { id: contentId, duration: since(tPoster) }); + + // Animate content back in with delay + contentOpacity.value = withDelay(200, withTiming(1, { + duration: 600, + easing: Easing.out(Easing.cubic) + })); + buttonsOpacity.value = withDelay(400, withTiming(1, { + duration: 500, + easing: Easing.out(Easing.cubic) + })); + } else { + // If preload fails, still show the image but without animation + posterOpacity.value = 1; + posterScale.value = 1; + overlayOpacity.value = 0.15; + contentOpacity.value = 1; + buttonsOpacity.value = 1; + } + } + + // Load logo if available with enhanced timing + if (logoUrl) { + const tLogo = nowMs(); + // Try to preload but don't fail if it times out - still show the logo + const logoSuccess = await preloadImage(logoUrl); + if (logoSuccess) { + logger.debug('[FeaturedContent] logo:preload:success', { id: contentId, duration: since(tLogo) }); + } else { + logger.debug('[FeaturedContent] logo:preload:failed', { id: contentId, duration: since(tLogo) }); + } + + // Always animate in the logo since we have the URL + logoOpacity.value = withDelay(500, withTiming(1, { + duration: 600, + easing: Easing.out(Easing.cubic) + })); + logger.debug('[FeaturedContent] logo:animated', { id: contentId }); + } + logger.info('[FeaturedContent] images:load:done', { id: contentId, total: since(t0) }); + }; + + loadImages(); + }, [featuredContent?.id, logoUrl]); + + const onLogoLoadError = () => { + setLogoLoaded(true); // Treat error as "loaded" to stop spinner + setLogoLoadError(true); + logger.warn('[FeaturedContent] logo:onError', { id: featuredContent?.id, url: logoUrl }); + }; + + const handleInfoPress = () => { + if (featuredContent) { + navigation.navigate('Metadata', { + id: featuredContent.id, + type: featuredContent.type + }); + } + }; // Show skeleton only if we're loading AND no content is available yet - if (loading && items.length === 0) { + if (loading && !featuredContent) { logger.debug('[FeaturedContent] render:loading', { sinceMount: since(firstRenderTsRef.current) }); return ; } - if (items.length === 0) { + if (!featuredContent) { // Suppress empty state while loading to avoid flash on startup/hydration logger.debug('[FeaturedContent] render:no-featured-content', { sinceMount: since(firstRenderTsRef.current) }); return ; } - const containerStyle = isTablet - ? [styles.tabletContainer as ViewStyle, { height: tabletHeroHeight }] - : styles.featuredContainer as ViewStyle; - - return ( - <> - - 1} - width={width} - height={isTablet ? tabletHeroHeight : height * 0.55} - snapEnabled={true} - pagingEnabled={true} - autoPlay={items.length > 1} - autoPlayInterval={10000} - data={items} - defaultScrollOffsetValue={scrollOffsetValue} - onProgressChange={progress} - style={{ width: "100%" }} - onScrollStart={() => { - logger.debug("Carousel scroll start"); - }} - onScrollEnd={() => { - logger.debug("Carousel scroll end"); - }} - onConfigurePanGesture={(g: { enabled: (arg0: boolean) => any }) => { - "worklet"; - g.enabled(true); - }} - onSnapToItem={(index: number) => { - logger.debug("Carousel current index:", index); - }} - renderItem={renderItem} - /> - - - {/* Pagination strictly below the hero container (not clipped by overflow) */} - {items.length > 1 && ( - - { - 'worklet'; - // Smooth scale for the active dot - let v = Math.abs(p - index); - if (index === 0 && p > length - 1) { - v = Math.abs(p - length); - } - const scale = interpolate(v, [0, 1], [1.2, 1], Extrapolation.CLAMP); - return { transform: [{ scale }] }; + { + navigation.navigate('Metadata', { + id: featuredContent.id, + type: featuredContent.type + }); }} + style={styles.tabletFullContainer as ViewStyle} + > + + + + + + + + + + {logoUrl && !logoLoadError ? ( + + + + ) : ( + + {featuredContent.name} + + )} + + + {featuredContent.genres?.slice(0, 4).map((genre, index, array) => ( + + + {genre} + + {index < array.length - 1 && ( + + )} + + ))} + + + {featuredContent.description && ( + + {featuredContent.description} + + )} + + + { + if (featuredContent) { + navigation.navigate('Streams', { + id: featuredContent.id, + type: featuredContent.type + }); + } + }} + activeOpacity={0.8} + > + + + Play Now + + + + + + + {isSaved ? "Saved" : "My List"} + + + + + + + More Info + + + + + + {/* Bottom fade to blend with background */} + - - )} - - ); + + ); + } else { + // Phone layout: original vertical stack + return ( + + { + navigation.navigate('Metadata', { + id: featuredContent.id, + type: featuredContent.type + }); + }} + style={styles.featuredContainer as ViewStyle} + > + + + + + + {logoUrl && !logoLoadError ? ( + + + + ) : ( + + {featuredContent.name} + + )} + + {featuredContent.genres?.slice(0, 3).map((genre, index, array) => ( + + + {genre} + + {index < array.length - 1 && ( + + )} + + ))} + + + + + + + + {isSaved ? "Saved" : "Save"} + + + + { + if (featuredContent) { + navigation.navigate('Streams', { + id: featuredContent.id, + type: featuredContent.type + }); + } + }} + activeOpacity={0.8} + > + + + Play + + + + + + + Info + + + + + + + + + {/* Bottom fade to blend with background */} + + + ); + } }; const styles = StyleSheet.create({ diff --git a/src/screens/HomeScreen.tsx b/src/screens/HomeScreen.tsx index dff0c61..b47b340 100644 --- a/src/screens/HomeScreen.tsx +++ b/src/screens/HomeScreen.tsx @@ -637,8 +637,8 @@ const HomeScreen = () => { ) : ( <>