From f7c0c670d76b938093f12c508327da92f875b656 Mon Sep 17 00:00:00 2001 From: tapframe Date: Sun, 19 Oct 2025 19:46:55 +0530 Subject: [PATCH] metadatascreen tablet layout overhaul --- .../home/ContinueWatchingSection.tsx | 205 +++++++- src/components/home/ThisWeekSection.tsx | 197 ++++++-- src/components/metadata/CastSection.tsx | 157 +++++- src/components/metadata/CommentsSection.tsx | 257 ++++++++-- src/components/metadata/MetadataDetails.tsx | 207 +++++++- src/components/metadata/RatingsSection.tsx | 70 ++- src/components/metadata/SeriesContent.tsx | 461 +++++++++++++----- src/components/metadata/TrailersSection.tsx | 220 +++++++-- src/screens/MetadataScreen.tsx | 254 +++++++--- 9 files changed, 1679 insertions(+), 349 deletions(-) diff --git a/src/components/home/ContinueWatchingSection.tsx b/src/components/home/ContinueWatchingSection.tsx index ce21382..5ed400d 100644 --- a/src/components/home/ContinueWatchingSection.tsx +++ b/src/components/home/ContinueWatchingSection.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, useRef } from 'react'; +import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react'; import { View, Text, @@ -39,6 +39,14 @@ interface ContinueWatchingRef { refresh: () => Promise; } +// Enhanced responsive breakpoints for Continue Watching section +const BREAKPOINTS = { + phone: 0, + tablet: 768, + largeTablet: 1024, + tv: 1440, +}; + // Dynamic poster calculation based on screen width for Continue Watching section const calculatePosterLayout = (screenWidth: number) => { const MIN_POSTER_WIDTH = 120; // Slightly larger for continue watching items @@ -96,6 +104,78 @@ const ContinueWatchingSection = React.forwardRef((props, re const [deletingItemId, setDeletingItemId] = useState(null); const longPressTimeoutRef = useRef(null); + // Enhanced responsive sizing for tablets and TV screens + const deviceWidth = Dimensions.get('window').width; + const deviceHeight = Dimensions.get('window').height; + + // Determine device type based on width + const getDeviceType = useCallback(() => { + if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; + if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; + if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; + return 'phone'; + }, [deviceWidth]); + + const deviceType = getDeviceType(); + const isTablet = deviceType === 'tablet'; + const isLargeTablet = deviceType === 'largeTablet'; + const isTV = deviceType === 'tv'; + const isLargeScreen = isTablet || isLargeTablet || isTV; + + // Enhanced responsive sizing for continue watching items + const computedItemWidth = useMemo(() => { + switch (deviceType) { + case 'tv': + return 400; // Larger items for TV + case 'largeTablet': + return 350; // Medium-large items for large tablets + case 'tablet': + return 320; // Medium items for tablets + default: + return 280; // Original phone size + } + }, [deviceType]); + + const computedItemHeight = useMemo(() => { + switch (deviceType) { + case 'tv': + return 160; // Taller items for TV + case 'largeTablet': + return 140; // Medium-tall items for large tablets + case 'tablet': + return 130; // Medium items for tablets + default: + return 120; // Original phone height + } + }, [deviceType]); + + // Enhanced spacing and padding + const horizontalPadding = useMemo(() => { + switch (deviceType) { + case 'tv': + return 32; + case 'largeTablet': + return 28; + case 'tablet': + return 24; + default: + return 16; // phone + } + }, [deviceType]); + + const itemSpacing = useMemo(() => { + switch (deviceType) { + case 'tv': + return 20; + case 'largeTablet': + return 18; + case 'tablet': + return 16; + default: + return 16; // phone + } + }, [deviceType]); + // Alert state for CustomAlert const [alertVisible, setAlertVisible] = useState(false); const [alertTitle, setAlertTitle] = useState(''); @@ -632,18 +712,28 @@ const ContinueWatchingSection = React.forwardRef((props, re // Memoized render function for continue watching items const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => ( handleContentPress(item.id, item.type)} onLongPress={() => handleLongPress(item)} delayLongPress={800} > {/* Poster Image */} - + ((props, re {/* Content Details */} - + {(() => { const isUpNext = item.type === 'series' && item.progress === 0; return ( {item.name} {isUpNext && ( - - Up Next + + Up Next )} @@ -690,12 +801,24 @@ const ContinueWatchingSection = React.forwardRef((props, re if (item.type === 'series' && item.season && item.episode) { return ( - + Season {item.season} {item.episodeTitle && ( {item.episodeTitle} @@ -705,7 +828,13 @@ const ContinueWatchingSection = React.forwardRef((props, re ); } else { return ( - + {item.year} • {item.type === 'movie' ? 'Movie' : 'Series'} ); @@ -715,7 +844,12 @@ const ContinueWatchingSection = React.forwardRef((props, re {/* Progress Bar */} {item.progress > 0 && ( - + ((props, re ]} /> - + {Math.round(item.progress)}% watched )} - ), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId]); + ), [currentTheme.colors, handleContentPress, handleLongPress, deletingItemId, computedItemWidth, computedItemHeight, isTV, isLargeTablet, isTablet]); // Memoized key extractor const keyExtractor = useCallback((item: ContinueWatchingItem) => `continue-${item.id}-${item.type}`, []); // Memoized item separator - const ItemSeparator = useCallback(() => , []); + const ItemSeparator = useCallback(() => , [itemSpacing]); // If no continue watching items, don't render anything if (continueWatchingItems.length === 0) { @@ -751,10 +891,23 @@ const ContinueWatchingSection = React.forwardRef((props, re style={styles.container} entering={FadeIn.duration(350)} > - + - Continue Watching - + Continue Watching + @@ -764,7 +917,13 @@ const ContinueWatchingSection = React.forwardRef((props, re keyExtractor={keyExtractor} horizontal showsHorizontalScrollIndicator={false} - contentContainerStyle={styles.wideList} + contentContainerStyle={[ + styles.wideList, + { + paddingLeft: horizontalPadding, + paddingRight: horizontalPadding + } + ]} ItemSeparatorComponent={ItemSeparator} onEndReachedThreshold={0.7} onEndReached={() => {}} @@ -792,7 +951,6 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - paddingHorizontal: 16, marginBottom: 16, }, titleContainer: { @@ -814,7 +972,6 @@ const styles = StyleSheet.create({ opacity: 0.8, }, wideList: { - paddingHorizontal: 16, paddingBottom: 8, paddingTop: 4, }, diff --git a/src/components/home/ThisWeekSection.tsx b/src/components/home/ThisWeekSection.tsx index b58d5b9..f9a53a6 100644 --- a/src/components/home/ThisWeekSection.tsx +++ b/src/components/home/ThisWeekSection.tsx @@ -28,6 +28,14 @@ const { width } = Dimensions.get('window'); const ITEM_WIDTH = width * 0.75; // phone default const ITEM_HEIGHT = 180; // phone default +// Enhanced responsive breakpoints +const BREAKPOINTS = { + phone: 0, + tablet: 768, + largeTablet: 1024, + tv: 1440, +}; + interface ThisWeekEpisode { id: string; seriesId: string; @@ -49,11 +57,77 @@ export const ThisWeekSection = React.memo(() => { const { currentTheme } = useTheme(); const { calendarData, loading } = useCalendarData(); - // Responsive sizing for tablets + // Enhanced responsive sizing for tablets and TV screens const deviceWidth = Dimensions.get('window').width; - const isTablet = deviceWidth >= 768; - const computedItemWidth = useMemo(() => (isTablet ? Math.min(deviceWidth * 0.46, 560) : ITEM_WIDTH), [isTablet, deviceWidth]); - const computedItemHeight = useMemo(() => (isTablet ? 220 : ITEM_HEIGHT), [isTablet]); + const deviceHeight = Dimensions.get('window').height; + + // Determine device type based on width + const getDeviceType = useCallback(() => { + if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; + if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; + if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; + return 'phone'; + }, [deviceWidth]); + + const deviceType = getDeviceType(); + const isTablet = deviceType === 'tablet'; + const isLargeTablet = deviceType === 'largeTablet'; + const isTV = deviceType === 'tv'; + const isLargeScreen = isTablet || isLargeTablet || isTV; + + // Enhanced responsive sizing + const computedItemWidth = useMemo(() => { + switch (deviceType) { + case 'tv': + return Math.min(deviceWidth * 0.25, 400); // 4 items per row on TV + case 'largeTablet': + return Math.min(deviceWidth * 0.35, 350); // 3 items per row on large tablet + case 'tablet': + return Math.min(deviceWidth * 0.46, 300); // 2 items per row on tablet + default: + return ITEM_WIDTH; // phone + } + }, [deviceType, deviceWidth]); + + const computedItemHeight = useMemo(() => { + switch (deviceType) { + case 'tv': + return 280; + case 'largeTablet': + return 250; + case 'tablet': + return 220; + default: + return ITEM_HEIGHT; // phone + } + }, [deviceType]); + + // Enhanced spacing and padding + const horizontalPadding = useMemo(() => { + switch (deviceType) { + case 'tv': + return 32; + case 'largeTablet': + return 28; + case 'tablet': + return 24; + default: + return 16; // phone + } + }, [deviceType]); + + const itemSpacing = useMemo(() => { + switch (deviceType) { + case 'tv': + return 20; + case 'largeTablet': + return 18; + case 'tablet': + return 16; + default: + return 16; // phone + } + }, [deviceType]); // Use the already memory-optimized calendar data instead of fetching separately const thisWeekEpisodes = useMemo(() => { @@ -144,35 +218,70 @@ export const ThisWeekSection = React.memo(() => { 'rgba(0,0,0,0.8)', 'rgba(0,0,0,0.95)' ]} - style={styles.gradient} + style={[ + styles.gradient, + { + padding: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12 + } + ]} locations={[0, 0.4, 0.6, 0.8, 1]} > {/* Content area */} - + {item.seriesName} - + {item.title} {item.overview && ( - + {item.overview} )} - + S{item.season}:E{item.episode} • - + {formattedDate} @@ -189,14 +298,43 @@ export const ThisWeekSection = React.memo(() => { style={styles.container} entering={FadeIn.duration(350)} > - + - This Week - + This Week + - - View All - + + View All + @@ -206,20 +344,26 @@ export const ThisWeekSection = React.memo(() => { renderItem={renderEpisodeItem} horizontal showsHorizontalScrollIndicator={false} - contentContainerStyle={[styles.listContent, { paddingLeft: isTablet ? 24 : 16, paddingRight: isTablet ? 24 : 16 }]} - snapToInterval={computedItemWidth + 16} + contentContainerStyle={[ + styles.listContent, + { + paddingLeft: horizontalPadding, + paddingRight: horizontalPadding + } + ]} + snapToInterval={computedItemWidth + itemSpacing} decelerationRate="fast" snapToAlignment="start" - initialNumToRender={isTablet ? 4 : 3} - windowSize={3} - maxToRenderPerBatch={3} + initialNumToRender={isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3} + windowSize={isTV ? 4 : isLargeTablet ? 4 : 3} + maxToRenderPerBatch={isTV ? 4 : isLargeTablet ? 4 : 3} removeClippedSubviews getItemLayout={(data, index) => { - const length = computedItemWidth + 16; + const length = computedItemWidth + itemSpacing; const offset = length * index; return { length, offset, index }; }} - ItemSeparatorComponent={() => } + ItemSeparatorComponent={() => } /> ); @@ -233,7 +377,6 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - paddingHorizontal: 16, marginBottom: 16, }, titleContainer: { @@ -269,8 +412,6 @@ const styles = StyleSheet.create({ marginRight: 4, }, listContent: { - paddingLeft: 16, - paddingRight: 16, paddingBottom: 8, }, loadingContainer: { @@ -316,7 +457,7 @@ const styles = StyleSheet.create({ padding: 12, borderRadius: 16, }, - contentArea: { + contentArea: { width: '100%', }, seriesName: { diff --git a/src/components/metadata/CastSection.tsx b/src/components/metadata/CastSection.tsx index 00f4c19..3f24812 100644 --- a/src/components/metadata/CastSection.tsx +++ b/src/components/metadata/CastSection.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import { View, Text, @@ -6,6 +6,7 @@ import { FlatList, TouchableOpacity, ActivityIndicator, + Dimensions, } from 'react-native'; import FastImage from '@d11/react-native-fast-image'; import Animated, { @@ -13,6 +14,14 @@ import Animated, { } from 'react-native-reanimated'; import { useTheme } from '../../contexts/ThemeContext'; +// Enhanced responsive breakpoints for Cast Section +const BREAKPOINTS = { + phone: 0, + tablet: 768, + largeTablet: 1024, + tv: 1440, +}; + interface CastSectionProps { cast: any[]; loadingCast: boolean; @@ -28,6 +37,78 @@ export const CastSection: React.FC = ({ }) => { const { currentTheme } = useTheme(); + // Enhanced responsive sizing for tablets and TV screens + const deviceWidth = Dimensions.get('window').width; + const deviceHeight = Dimensions.get('window').height; + + // Determine device type based on width + const getDeviceType = useCallback(() => { + if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; + if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; + if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; + return 'phone'; + }, [deviceWidth]); + + const deviceType = getDeviceType(); + const isTablet = deviceType === 'tablet'; + const isLargeTablet = deviceType === 'largeTablet'; + const isTV = deviceType === 'tv'; + const isLargeScreen = isTablet || isLargeTablet || isTV; + + // Enhanced spacing and padding + const horizontalPadding = useMemo(() => { + switch (deviceType) { + case 'tv': + return 32; + case 'largeTablet': + return 28; + case 'tablet': + return 24; + default: + return 16; // phone + } + }, [deviceType]); + + // Enhanced cast card sizing + const castCardWidth = useMemo(() => { + switch (deviceType) { + case 'tv': + return 120; + case 'largeTablet': + return 110; + case 'tablet': + return 100; + default: + return 90; // phone + } + }, [deviceType]); + + const castImageSize = useMemo(() => { + switch (deviceType) { + case 'tv': + return 100; + case 'largeTablet': + return 90; + case 'tablet': + return 85; + default: + return 80; // phone + } + }, [deviceType]); + + const castCardSpacing = useMemo(() => { + switch (deviceType) { + case 'tv': + return 20; + case 'largeTablet': + return 18; + case 'tablet': + return 16; + default: + return 16; // phone + } + }, [deviceType]); + if (loadingCast) { return ( @@ -45,25 +126,52 @@ export const CastSection: React.FC = ({ style={styles.castSection} entering={FadeIn.duration(300).delay(150)} > - - Cast + + Cast item.id.toString()} renderItem={({ item, index }) => ( onSelectCastMember(item)} activeOpacity={0.7} > - + {item.profile_path ? ( = ({ resizeMode={FastImage.resizeMode.cover} /> ) : ( - - + + {item.name.split(' ').reduce((prev: string, current: string) => prev + current[0], '').substring(0, 2)} )} - {item.name} + {item.name} {isTmdbEnrichmentEnabled && item.character && ( - {item.character} + {item.character} )} @@ -107,14 +242,12 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'space-between', marginBottom: 12, - paddingHorizontal: 16, }, sectionTitle: { fontSize: 18, fontWeight: '700', }, castList: { - paddingHorizontal: 16, paddingBottom: 4, }, castCard: { diff --git a/src/components/metadata/CommentsSection.tsx b/src/components/metadata/CommentsSection.tsx index 60b99c6..de9b31d 100644 --- a/src/components/metadata/CommentsSection.tsx +++ b/src/components/metadata/CommentsSection.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useRef } from 'react'; +import React, { useCallback, useState, useRef, useMemo } from 'react'; import { View, Text, @@ -21,7 +21,13 @@ import { useTraktComments } from '../../hooks/useTraktComments'; import { useSettings } from '../../hooks/useSettings'; import BottomSheet, { BottomSheetView, BottomSheetScrollView } from '@gorhom/bottom-sheet'; -const { width } = Dimensions.get('window'); +// Enhanced responsive breakpoints for Comments Section +const BREAKPOINTS = { + phone: 0, + tablet: 768, + largeTablet: 1024, + tv: 1440, +}; interface CommentsSectionProps { imdbId: string; @@ -191,6 +197,64 @@ const CompactCommentCard: React.FC<{ }).start(); }, [fadeInOpacity]); + // Enhanced responsive sizing for tablets and TV screens + const deviceWidth = Dimensions.get('window').width; + const deviceHeight = Dimensions.get('window').height; + + // Determine device type based on width + const getDeviceType = useCallback(() => { + if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; + if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; + if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; + return 'phone'; + }, [deviceWidth]); + + const deviceType = getDeviceType(); + const isTablet = deviceType === 'tablet'; + const isLargeTablet = deviceType === 'largeTablet'; + const isTV = deviceType === 'tv'; + const isLargeScreen = isTablet || isLargeTablet || isTV; + + // Enhanced comment card sizing + const commentCardWidth = useMemo(() => { + switch (deviceType) { + case 'tv': + return 360; + case 'largeTablet': + return 320; + case 'tablet': + return 300; + default: + return 280; // phone + } + }, [deviceType]); + + const commentCardHeight = useMemo(() => { + switch (deviceType) { + case 'tv': + return 200; + case 'largeTablet': + return 185; + case 'tablet': + return 175; + default: + return 170; // phone + } + }, [deviceType]); + + const commentCardSpacing = useMemo(() => { + switch (deviceType) { + case 'tv': + return 16; + case 'largeTablet': + return 14; + case 'tablet': + return 12; + default: + return 12; // phone + } + }, [deviceType]); + // Safety check - ensure comment data exists if (!comment || !comment.comment) { return null; @@ -272,6 +336,11 @@ const CompactCommentCard: React.FC<{ borderColor: theme.colors.border, opacity: fadeInOpacity, transform: isPressed ? [{ scale: 0.98 }] : [{ scale: 1 }], + width: commentCardWidth, + height: commentCardHeight, + marginRight: commentCardSpacing, + padding: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12, + borderRadius: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12 }, ]} > @@ -287,18 +356,41 @@ const CompactCommentCard: React.FC<{ > {/* Trakt Icon - Top Right Corner */} - + {/* Header Section - Fixed at top */} - + - + {username} {user.vip && ( - - VIP + + VIP )} @@ -306,48 +398,107 @@ const CompactCommentCard: React.FC<{ {/* Rating - Show stars */} {comment.user_stats?.rating && ( - + {renderCompactStars(comment.user_stats.rating)} - + {comment.user_stats.rating}/10 )} {/* Comment Preview - Flexible area that fills space */} - + {shouldBlurContent ? ( - ⚠️ This comment contains spoilers. Tap to reveal. + ⚠️ This comment contains spoilers. Tap to reveal. ) : ( )} {/* Meta Info - Fixed at bottom */} - + {comment.spoiler && ( - Spoiler + Spoiler )} - + {formatRelativeTime(comment.created_at)} {comment.likes > 0 && ( - + 👍 {comment.likes} )} {comment.replies > 0 && ( - + 💬 {comment.replies} )} @@ -578,6 +729,38 @@ export const CommentsSection: React.FC = ({ const { settings } = useSettings(); const [hasLoadedOnce, setHasLoadedOnce] = React.useState(false); + // Enhanced responsive sizing for tablets and TV screens + const deviceWidth = Dimensions.get('window').width; + const deviceHeight = Dimensions.get('window').height; + + // Determine device type based on width + const getDeviceType = useCallback(() => { + if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; + if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; + if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; + return 'phone'; + }, [deviceWidth]); + + const deviceType = getDeviceType(); + const isTablet = deviceType === 'tablet'; + const isLargeTablet = deviceType === 'largeTablet'; + const isTV = deviceType === 'tv'; + const isLargeScreen = isTablet || isLargeTablet || isTV; + + // Enhanced spacing and padding + const horizontalPadding = useMemo(() => { + switch (deviceType) { + case 'tv': + return 32; + case 'largeTablet': + return 28; + case 'tablet': + return 24; + default: + return 16; // phone + } + }, [deviceType]); + const { comments, loading, @@ -705,9 +888,23 @@ export const CommentsSection: React.FC = ({ } return ( - - - + + + Trakt Comments @@ -744,11 +941,14 @@ export const CommentsSection: React.FC = ({ renderItem={renderComment} contentContainerStyle={styles.horizontalList} removeClippedSubviews={false} - getItemLayout={(data, index) => ({ - length: 292, // width + marginRight - offset: 292 * index, - index, - })} + getItemLayout={(data, index) => { + const itemWidth = isTV ? 376 : isLargeTablet ? 334 : isTablet ? 312 : 292; // width + marginRight + return { + length: itemWidth, + offset: itemWidth * index, + index, + }; + }} onEndReached={() => { if (hasMore && !loading) { loadMore(); @@ -991,7 +1191,6 @@ export const CommentBottomSheet: React.FC<{ const styles = StyleSheet.create({ container: { - padding: 16, marginBottom: 24, }, header: { @@ -1008,11 +1207,7 @@ const styles = StyleSheet.create({ paddingRight: 16, }, compactCard: { - width: 280, - height: 170, - padding: 12, paddingBottom: 16, - marginRight: 12, borderRadius: 12, borderWidth: 1, shadowColor: '#000', diff --git a/src/components/metadata/MetadataDetails.tsx b/src/components/metadata/MetadataDetails.tsx index 14463d7..3c7c0fe 100644 --- a/src/components/metadata/MetadataDetails.tsx +++ b/src/components/metadata/MetadataDetails.tsx @@ -1,10 +1,11 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, + Dimensions, } from 'react-native'; import { MaterialIcons } from '@expo/vector-icons'; import FastImage from '@d11/react-native-fast-image'; @@ -20,6 +21,15 @@ import Animated, { import { useTheme } from '../../contexts/ThemeContext'; import { isMDBListEnabled } from '../../screens/MDBListSettingsScreen'; import { getAgeRatingColor } from '../../utils/ageRatingColors'; + +// Enhanced responsive breakpoints for Metadata Details +const BREAKPOINTS = { + phone: 0, + tablet: 768, + largeTablet: 1024, + tv: 1440, +}; + // MetadataSourceSelector removed interface MetadataDetailsProps { @@ -45,6 +55,38 @@ const MetadataDetails: React.FC = ({ const [isMDBEnabled, setIsMDBEnabled] = useState(false); const [isTextTruncated, setIsTextTruncated] = useState(false); + // Enhanced responsive sizing for tablets and TV screens + const deviceWidth = Dimensions.get('window').width; + const deviceHeight = Dimensions.get('window').height; + + // Determine device type based on width + const getDeviceType = useCallback(() => { + if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; + if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; + if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; + return 'phone'; + }, [deviceWidth]); + + const deviceType = getDeviceType(); + const isTablet = deviceType === 'tablet'; + const isLargeTablet = deviceType === 'largeTablet'; + const isTV = deviceType === 'tv'; + const isLargeScreen = isTablet || isLargeTablet || isTV; + + // Enhanced spacing and padding + const horizontalPadding = useMemo(() => { + switch (deviceType) { + case 'tv': + return 32; + case 'largeTablet': + return 28; + case 'tablet': + return 24; + default: + return 16; // phone + } + }, [deviceType]); + // Animation values for smooth height transition const animatedHeight = useSharedValue(0); const [measuredHeights, setMeasuredHeights] = useState({ collapsed: 0, expanded: 0 }); @@ -144,12 +186,31 @@ function formatRuntime(runtime: string): string { )} {/* Meta Info */} - + {metadata.year && ( - {metadata.year} + {metadata.year} )} {metadata.runtime && ( - + {formatRuntime(metadata.runtime)} )} @@ -157,17 +218,32 @@ function formatRuntime(runtime: string): string { {metadata.certification} )} {metadata.imdbRating && !isMDBEnabled && ( - {metadata.imdbRating} + {metadata.imdbRating} )} @@ -178,18 +254,62 @@ function formatRuntime(runtime: string): string { {/* Creator/Director Info */} {metadata.directors && metadata.directors.length > 0 && ( - - Director{metadata.directors.length > 1 ? 's' : ''}: - {metadata.directors.join(', ')} + + Director{metadata.directors.length > 1 ? 's' : ''}: + {metadata.directors.join(', ')} )} {metadata.creators && metadata.creators.length > 0 && ( - - Creator{metadata.creators.length > 1 ? 's' : ''}: - {metadata.creators.join(', ')} + + Creator{metadata.creators.length > 1 ? 's' : ''}: + {metadata.creators.join(', ')} )} @@ -197,19 +317,41 @@ function formatRuntime(runtime: string): string { {/* Description */} {metadata.description && ( {/* Hidden text elements to measure heights */} {metadata.description} {metadata.description} @@ -222,7 +364,14 @@ function formatRuntime(runtime: string): string { > @@ -230,13 +379,25 @@ function formatRuntime(runtime: string): string { {(isTextTruncated || isFullDescriptionOpen) && ( - - + + {isFullDescriptionOpen ? 'Show Less' : 'Show More'} @@ -267,8 +428,6 @@ const styles = StyleSheet.create({ metaInfo: { flexDirection: 'row', alignItems: 'center', - gap: 18, - paddingHorizontal: 16, marginBottom: 12, }, metaText: { @@ -303,7 +462,6 @@ const styles = StyleSheet.create({ }, creatorContainer: { marginBottom: 2, - paddingHorizontal: 16, }, creatorSection: { flexDirection: 'row', @@ -324,7 +482,6 @@ const styles = StyleSheet.create({ }, descriptionContainer: { marginBottom: 16, - paddingHorizontal: 16, }, description: { fontSize: 15, diff --git a/src/components/metadata/RatingsSection.tsx b/src/components/metadata/RatingsSection.tsx index f8aaab9..8d73b14 100644 --- a/src/components/metadata/RatingsSection.tsx +++ b/src/components/metadata/RatingsSection.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState, useRef } from 'react'; -import { View, Text, StyleSheet, ActivityIndicator, Image, Animated } from 'react-native'; +import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react'; +import { View, Text, StyleSheet, ActivityIndicator, Image, Animated, Dimensions } from 'react-native'; import { useTheme } from '../../contexts/ThemeContext'; import { useMDBListRatings } from '../../hooks/useMDBListRatings'; import AsyncStorage from '@react-native-async-storage/async-storage'; @@ -13,6 +13,14 @@ import TMDBIcon from '../../../assets/rating-icons/tmdb.svg'; import TraktIcon from '../../../assets/rating-icons/trakt.svg'; import AudienceScoreIcon from '../../../assets/rating-icons/audienscore.png'; +// Enhanced responsive breakpoints for Ratings Section +const BREAKPOINTS = { + phone: 0, + tablet: 768, + largeTablet: 1024, + tv: 1440, +}; + export const RATING_PROVIDERS = { imdb: { name: 'IMDb', @@ -56,6 +64,50 @@ export const RatingsSection: React.FC = ({ imdbId, type }) const fadeAnim = useRef(new Animated.Value(0)).current; const { currentTheme } = useTheme(); + // Responsive device type + const deviceWidth = Dimensions.get('window').width; + const getDeviceType = useCallback(() => { + if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; + if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; + if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; + return 'phone'; + }, [deviceWidth]); + + const deviceType = getDeviceType(); + const isTablet = deviceType === 'tablet'; + const isLargeTablet = deviceType === 'largeTablet'; + const isTV = deviceType === 'tv'; + + const horizontalPadding = useMemo(() => { + switch (deviceType) { + case 'tv': + return 32; + case 'largeTablet': + return 28; + case 'tablet': + return 24; + default: + return 16; + } + }, [deviceType]); + + const iconSize = useMemo(() => { + switch (deviceType) { + case 'tv': + return 20; + case 'largeTablet': + return 18; + case 'tablet': + return 16; + default: + return 16; + } + }, [deviceType]); + + const textSize = useMemo(() => (isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14), [isTV, isLargeTablet, isTablet]); + const itemSpacing = useMemo(() => (isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12), [isTV, isLargeTablet, isTablet]); + const iconTextGap = useMemo(() => (isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4), [isTV, isLargeTablet, isTablet]); + useEffect(() => { loadProviderSettings(); checkMDBListEnabled(); @@ -164,6 +216,7 @@ export const RatingsSection: React.FC = ({ imdbId, type }) style={[ styles.container, { + paddingHorizontal: horizontalPadding, opacity: fadeAnim, transform: [{ translateY: fadeAnim.interpolate({ @@ -180,22 +233,22 @@ export const RatingsSection: React.FC = ({ imdbId, type }) const displayValue = config.transform(parseFloat(value as string)); return ( - + {config.isImage ? ( ) : ( - + {React.createElement(config.icon as any, { - width: 16, - height: 16, + width: iconSize, + height: iconSize, })} )} - + {displayValue} @@ -210,7 +263,6 @@ const styles = StyleSheet.create({ container: { marginTop: 2, marginBottom: 8, - paddingHorizontal: 16, }, loadingContainer: { height: 40, diff --git a/src/components/metadata/SeriesContent.tsx b/src/components/metadata/SeriesContent.tsx index 9d20443..93a7e52 100644 --- a/src/components/metadata/SeriesContent.tsx +++ b/src/components/metadata/SeriesContent.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useRef } from 'react'; +import React, { useEffect, useState, useRef, useCallback, useMemo } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ActivityIndicator, Dimensions, useWindowDimensions, useColorScheme, FlatList } from 'react-native'; import FastImage from '@d11/react-native-fast-image'; import { MaterialIcons } from '@expo/vector-icons'; @@ -15,6 +15,14 @@ import { TraktService } from '../../services/traktService'; import { logger } from '../../utils/logger'; import AsyncStorage from '@react-native-async-storage/async-storage'; +// Enhanced responsive breakpoints for Seasons Section +const BREAKPOINTS = { + phone: 0, + tablet: 768, + largeTablet: 1024, + tv: 1440, +}; + interface SeriesContentProps { episodes: Episode[]; selectedSeason: number; @@ -42,8 +50,80 @@ export const SeriesContent: React.FC = ({ const { currentTheme } = useTheme(); const { settings } = useSettings(); const { width } = useWindowDimensions(); - const isTablet = width > 768; const isDarkMode = useColorScheme() === 'dark'; + + // Enhanced responsive sizing for tablets and TV screens + const deviceWidth = Dimensions.get('window').width; + const deviceHeight = Dimensions.get('window').height; + + // Determine device type based on width + const getDeviceType = useCallback(() => { + if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; + if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; + if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; + return 'phone'; + }, [deviceWidth]); + + const deviceType = getDeviceType(); + const isTablet = deviceType === 'tablet'; + const isLargeTablet = deviceType === 'largeTablet'; + const isTV = deviceType === 'tv'; + const isLargeScreen = isTablet || isLargeTablet || isTV; + + // Enhanced spacing and padding for seasons section + const horizontalPadding = useMemo(() => { + switch (deviceType) { + case 'tv': + return 32; + case 'largeTablet': + return 28; + case 'tablet': + return 24; + default: + return 16; // phone + } + }, [deviceType]); + + // Enhanced season poster sizing + const seasonPosterWidth = useMemo(() => { + switch (deviceType) { + case 'tv': + return 140; + case 'largeTablet': + return 130; + case 'tablet': + return 120; + default: + return 100; // phone + } + }, [deviceType]); + + const seasonPosterHeight = useMemo(() => { + switch (deviceType) { + case 'tv': + return 210; + case 'largeTablet': + return 195; + case 'tablet': + return 180; + default: + return 150; // phone + } + }, [deviceType]); + + const seasonButtonSpacing = useMemo(() => { + switch (deviceType) { + case 'tv': + return 20; + case 'largeTablet': + return 18; + case 'tablet': + return 16; + default: + return 16; // phone + } + }, [deviceType]); + const [episodeProgress, setEpisodeProgress] = useState<{ [key: string]: { currentTime: number; duration: number; lastUpdated: number } }>({}); // Delay item entering animations to avoid FlashList initial layout glitches const [enableItemAnimations, setEnableItemAnimations] = useState(false); @@ -342,12 +422,22 @@ export const SeriesContent: React.FC = ({ const seasons = Object.keys(groupedEpisodes).map(Number).sort((a, b) => a - b); return ( - - + + Seasons {/* Dropdown Toggle Button */} @@ -360,7 +450,10 @@ export const SeriesContent: React.FC = ({ : currentTheme.colors.elevation3, borderColor: seasonViewMode === 'posters' ? 'rgba(255,255,255,0.2)' - : 'rgba(255,255,255,0.3)' + : 'rgba(255,255,255,0.3)', + paddingHorizontal: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8, + paddingVertical: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4, + borderRadius: isTV ? 10 : isLargeTablet ? 8 : isTablet ? 6 : 6 } ]} onPress={() => { @@ -375,7 +468,8 @@ export const SeriesContent: React.FC = ({ { color: seasonViewMode === 'posters' ? currentTheme.colors.mediumEmphasis - : currentTheme.colors.highEmphasis + : currentTheme.colors.highEmphasis, + fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 12 } ]}> {seasonViewMode === 'posters' ? 'Posters' : 'Text'} @@ -389,7 +483,12 @@ export const SeriesContent: React.FC = ({ horizontal showsHorizontalScrollIndicator={false} style={styles.seasonSelectorContainer} - contentContainerStyle={[styles.seasonSelectorContent, isTablet && styles.seasonSelectorContentTablet]} + contentContainerStyle={[ + styles.seasonSelectorContent, + { + paddingBottom: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 + } + ]} initialNumToRender={5} maxToRenderPerBatch={5} windowSize={3} @@ -416,7 +515,13 @@ export const SeriesContent: React.FC = ({ onSeasonChange(season)} @@ -448,12 +553,23 @@ export const SeriesContent: React.FC = ({ onSeasonChange(season)} > - + = ({ {selectedSeason === season && ( )} @@ -471,18 +589,19 @@ export const SeriesContent: React.FC = ({ Season {season} - + ); }} @@ -550,22 +669,43 @@ export const SeriesContent: React.FC = ({ key={episode.id} style={[ styles.episodeCardVertical, - { backgroundColor: currentTheme.colors.elevation2 } + { + backgroundColor: currentTheme.colors.elevation2, + borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16, + marginBottom: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16, + height: isTV ? 200 : isLargeTablet ? 180 : isTablet ? 160 : 120 + } ]} onPress={() => onSelectEpisode(episode)} activeOpacity={0.7} > - - {episodeString} + + {episodeString} {showProgress && ( @@ -578,53 +718,98 @@ export const SeriesContent: React.FC = ({ )} {progressPercent >= 85 && ( - - + + )} + { + color: currentTheme.colors.text, + fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15, + lineHeight: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 18, + marginBottom: isTV ? 4 : isLargeTablet ? 3 : isTablet ? 2 : 2 + } + ]} numberOfLines={isLargeScreen ? 3 : 2}> {episode.name} {effectiveVote > 0 && ( - + {effectiveVote.toFixed(1)} )} {effectiveRuntime && ( - - + + {formatRuntime(effectiveRuntime)} )} {episode.air_date && ( - + {formatDate(episode.air_date)} )} @@ -632,9 +817,12 @@ export const SeriesContent: React.FC = ({ + { + color: currentTheme.colors.mediumEmphasis, + fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 13, + lineHeight: isTV ? 22 : isLargeTablet ? 20 : isTablet ? 20 : 18 + } + ]} numberOfLines={isLargeScreen ? 4 : isTablet ? 3 : 2}> {episode.overview || 'No description available'} @@ -684,16 +872,19 @@ export const SeriesContent: React.FC = ({ key={episode.id} style={[ styles.episodeCardHorizontal, - isTablet && styles.episodeCardHorizontalTablet, + { + borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16, + height: isTV ? 280 : isLargeTablet ? 260 : isTablet ? 240 : 200, + elevation: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 8, + shadowOpacity: isTV ? 0.4 : isLargeTablet ? 0.35 : isTablet ? 0.3 : 0.3, + shadowRadius: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 8 + }, // Gradient border styling { borderWidth: 1, borderColor: 'transparent', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, - shadowOpacity: 0.3, - shadowRadius: 8, - elevation: 12, } ]} onPress={() => onSelectEpisode(episode)} @@ -746,35 +937,88 @@ export const SeriesContent: React.FC = ({ style={styles.episodeGradient} > {/* Content Container */} - + {/* Episode Number Badge */} - - {episodeString} + + {episodeString} {/* Episode Title */} - + {episode.name} {/* Episode Description */} - + {episode.overview || 'No description available'} {/* Metadata Row */} - + {episode.runtime && ( - + {formatRuntime(episode.runtime)} )} {episode.vote_average > 0 && ( - - + + {episode.vote_average.toFixed(1)} @@ -799,10 +1043,18 @@ export const SeriesContent: React.FC = ({ {/* Completed Badge */} {progressPercent >= 85 && ( - - + + )} @@ -824,7 +1076,15 @@ export const SeriesContent: React.FC = ({ - + {currentSeasonEpisodes.length} {currentSeasonEpisodes.length === 1 ? 'Episode' : 'Episodes'} @@ -854,7 +1114,10 @@ export const SeriesContent: React.FC = ({ entering={enableItemAnimations ? FadeIn.duration(300).delay(100 + index * 30) : undefined as any} style={[ styles.episodeCardWrapperHorizontal, - isTablet && styles.episodeCardWrapperHorizontalTablet + { + width: isTV ? width * 0.45 : isLargeTablet ? width * 0.4 : isTablet ? width * 0.4 : width * 0.75, + marginRight: isTV ? 24 : isLargeTablet ? 20 : isTablet ? 20 : 16 + } ]} > {renderHorizontalEpisodeCard(episode)} @@ -863,14 +1126,20 @@ export const SeriesContent: React.FC = ({ keyExtractor={episode => episode.id.toString()} horizontal showsHorizontalScrollIndicator={false} - contentContainerStyle={isTablet ? styles.episodeListContentHorizontalTablet : styles.episodeListContentHorizontal} + contentContainerStyle={[ + styles.episodeListContentHorizontal, + { + paddingLeft: horizontalPadding, + paddingRight: horizontalPadding + } + ]} removeClippedSubviews initialNumToRender={3} maxToRenderPerBatch={5} windowSize={5} getItemLayout={(data, index) => { - const cardWidth = isTablet ? width * 0.4 : width * 0.75; - const margin = isTablet ? 20 : 16; + const cardWidth = isTV ? width * 0.45 : isLargeTablet ? width * 0.4 : isTablet ? width * 0.4 : width * 0.75; + const margin = isTV ? 24 : isLargeTablet ? 20 : isTablet ? 20 : 16; return { length: cardWidth + margin, offset: (cardWidth + margin) * index, @@ -892,7 +1161,13 @@ export const SeriesContent: React.FC = ({ )} keyExtractor={episode => episode.id.toString()} - contentContainerStyle={isTablet ? styles.episodeListContentVerticalTablet : styles.episodeListContentVertical} + contentContainerStyle={[ + styles.episodeListContentVertical, + { + paddingHorizontal: horizontalPadding, + paddingBottom: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 24 : 8 + } + ]} removeClippedSubviews /> ) @@ -937,11 +1212,6 @@ const styles = StyleSheet.create({ // Vertical Layout Styles episodeListContentVertical: { paddingBottom: 8, - paddingHorizontal: 16, - }, - episodeListContentVerticalTablet: { - paddingHorizontal: 16, - paddingBottom: 8, }, episodeGridVertical: { flexDirection: 'row', @@ -1098,20 +1368,10 @@ const styles = StyleSheet.create({ // Horizontal Layout Styles episodeListContentHorizontal: { - paddingLeft: 16, - paddingRight: 16, - }, - episodeListContentHorizontalTablet: { - paddingLeft: 24, - paddingRight: 24, + // Padding will be added responsively }, episodeCardWrapperHorizontal: { - width: Dimensions.get('window').width * 0.75, - marginRight: 16, - }, - episodeCardWrapperHorizontalTablet: { - width: Dimensions.get('window').width * 0.4, - marginRight: 20, + // Dimensions will be set responsively }, episodeCardHorizontal: { borderRadius: 16, @@ -1128,13 +1388,6 @@ const styles = StyleSheet.create({ width: '100%', backgroundColor: 'transparent', }, - episodeCardHorizontalTablet: { - height: 260, - borderRadius: 20, - elevation: 12, - shadowOpacity: 0.4, - shadowRadius: 16, - }, episodeBackgroundImage: { width: '100%', height: '100%', @@ -1273,11 +1526,6 @@ const styles = StyleSheet.create({ // Season Selector Styles seasonSelectorWrapper: { marginBottom: 20, - paddingHorizontal: 16, - }, - seasonSelectorWrapperTablet: { - marginBottom: 24, - paddingHorizontal: 24, }, seasonSelectorHeader: { flexDirection: 'row', @@ -1306,32 +1554,14 @@ const styles = StyleSheet.create({ }, seasonButton: { alignItems: 'center', - marginRight: 16, - width: 100, - }, - seasonButtonTablet: { - alignItems: 'center', - marginRight: 20, - width: 120, }, selectedSeasonButton: { opacity: 1, }, seasonPosterContainer: { position: 'relative', - width: 100, - height: 150, borderRadius: 8, overflow: 'hidden', - marginBottom: 8, - }, - seasonPosterContainerTablet: { - position: 'relative', - width: 120, - height: 180, - borderRadius: 12, - overflow: 'hidden', - marginBottom: 12, }, seasonPoster: { width: '100%', @@ -1382,22 +1612,7 @@ const styles = StyleSheet.create({ }, seasonTextButton: { alignItems: 'center', - marginRight: 16, - width: 110, justifyContent: 'center', - paddingVertical: 12, - paddingHorizontal: 16, - borderRadius: 12, - backgroundColor: 'transparent', - }, - seasonTextButtonTablet: { - alignItems: 'center', - marginRight: 20, - width: 130, - justifyContent: 'center', - paddingVertical: 14, - paddingHorizontal: 18, - borderRadius: 14, backgroundColor: 'transparent', }, selectedSeasonTextButton: { diff --git a/src/components/metadata/TrailersSection.tsx b/src/components/metadata/TrailersSection.tsx index c059a59..df1edd4 100644 --- a/src/components/metadata/TrailersSection.tsx +++ b/src/components/metadata/TrailersSection.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, memo, useRef } from 'react'; +import React, { useState, useEffect, useCallback, memo, useRef, useMemo } from 'react'; import { View, Text, @@ -21,8 +21,13 @@ import TrailerService from '../../services/trailerService'; import TrailerModal from './TrailerModal'; import Animated, { useSharedValue, withTiming, withDelay, useAnimatedStyle } from 'react-native-reanimated'; -const { width } = Dimensions.get('window'); -const isTablet = width >= 768; +// Enhanced responsive breakpoints for Trailers Section +const BREAKPOINTS = { + phone: 0, + tablet: 768, + largeTablet: 1024, + tv: 1440, +}; interface TrailerVideo { id: string; @@ -66,6 +71,65 @@ const TrailersSection: React.FC = memo(({ const [dropdownVisible, setDropdownVisible] = useState(false); const [backendAvailable, setBackendAvailable] = useState(null); + // Enhanced responsive sizing for tablets and TV screens + const deviceWidth = Dimensions.get('window').width; + const deviceHeight = Dimensions.get('window').height; + + // Determine device type based on width + const getDeviceType = useCallback(() => { + if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; + if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; + if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; + return 'phone'; + }, [deviceWidth]); + + const deviceType = getDeviceType(); + const isTablet = deviceType === 'tablet'; + const isLargeTablet = deviceType === 'largeTablet'; + const isTV = deviceType === 'tv'; + const isLargeScreen = isTablet || isLargeTablet || isTV; + + // Enhanced spacing and padding + const horizontalPadding = useMemo(() => { + switch (deviceType) { + case 'tv': + return 32; + case 'largeTablet': + return 28; + case 'tablet': + return 24; + default: + return 16; // phone + } + }, [deviceType]); + + // Enhanced trailer card sizing + const trailerCardWidth = useMemo(() => { + switch (deviceType) { + case 'tv': + return 240; + case 'largeTablet': + return 220; + case 'tablet': + return 200; + default: + return 170; // phone + } + }, [deviceType]); + + const trailerCardSpacing = useMemo(() => { + switch (deviceType) { + case 'tv': + return 16; + case 'largeTablet': + return 14; + case 'tablet': + return 12; + default: + return 12; // phone + } + }, [deviceType]); + // Smooth reveal animation after trailers are fetched const sectionOpacitySV = useSharedValue(0); const sectionTranslateYSV = useSharedValue(8); @@ -462,22 +526,48 @@ const TrailersSection: React.FC = memo(({ } return ( - + {/* Enhanced Header with Category Selector */} - + Trailers & Videos {/* Category Selector - Right Aligned */} {trailerCategories.length > 0 && selectedCategory && ( @@ -485,7 +575,7 @@ const TrailersSection: React.FC = memo(({ @@ -506,32 +596,58 @@ const TrailersSection: React.FC = memo(({ > {trailerCategories.map(category => ( handleCategorySelect(category)} activeOpacity={0.7} > - + {formatTrailerType(category)} - + {trailers[category].length} @@ -548,16 +664,25 @@ const TrailersSection: React.FC = memo(({ {trailers[selectedCategory].map((trailer, index) => ( handleTrailerPress(trailer)} activeOpacity={0.9} > @@ -565,33 +690,71 @@ const TrailersSection: React.FC = memo(({ {/* Subtle Gradient Overlay */} - + {/* Trailer Info */} - + {trailer.displayName || trailer.name} - + {new Date(trailer.published_at).getFullYear()} ))} {/* Scroll Indicator - shows when there are more items to scroll */} - {trailers[selectedCategory].length > (isTablet ? 4 : 3) && ( - + {trailers[selectedCategory].length > (isTV ? 5 : isLargeTablet ? 4 : isTablet ? 4 : 3) && ( + @@ -614,7 +777,6 @@ const TrailersSection: React.FC = memo(({ const styles = StyleSheet.create({ container: { - paddingHorizontal: 16, marginTop: 24, marginBottom: 16, }, @@ -749,13 +911,11 @@ const styles = StyleSheet.create({ }, trailersScrollContent: { paddingHorizontal: 4, // Restore padding for first/last items - gap: 12, paddingRight: 20, // Extra padding at end for scroll indicator }, // Enhanced Trailer Card Styles trailerCard: { - width: isTablet ? 200 : 170, backgroundColor: 'rgba(255,255,255,0.03)', borderRadius: 16, borderWidth: 1, diff --git a/src/screens/MetadataScreen.tsx b/src/screens/MetadataScreen.tsx index 7e94b3e..d347925 100644 --- a/src/screens/MetadataScreen.tsx +++ b/src/screens/MetadataScreen.tsx @@ -67,6 +67,14 @@ const MemoizedCastSection = memo(CastSection); const MemoizedSeriesContent = memo(SeriesContent); const MemoizedMovieContent = memo(MovieContent); const MemoizedMoreLikeThisSection = memo(MoreLikeThisSection); +// Enhanced responsive breakpoints for Metadata Screen +const BREAKPOINTS = { + phone: 0, + tablet: 768, + largeTablet: 1024, + tv: 1440, +}; + const MemoizedRatingsSection = memo(RatingsSection); const MemoizedCommentsSection = memo(CommentsSection); const MemoizedCastDetailsModal = memo(CastDetailsModal); @@ -90,6 +98,38 @@ const MetadataScreen: React.FC = () => { // Trakt integration const { isAuthenticated, isInWatchlist, isInCollection, addToWatchlist, removeFromWatchlist, addToCollection, removeFromCollection } = useTraktContext(); + // Enhanced responsive sizing for tablets and TV screens + const deviceWidth = Dimensions.get('window').width; + const deviceHeight = Dimensions.get('window').height; + + // Determine device type based on width + const getDeviceType = useCallback(() => { + if (deviceWidth >= BREAKPOINTS.tv) return 'tv'; + if (deviceWidth >= BREAKPOINTS.largeTablet) return 'largeTablet'; + if (deviceWidth >= BREAKPOINTS.tablet) return 'tablet'; + return 'phone'; + }, [deviceWidth]); + + const deviceType = getDeviceType(); + const isTablet = deviceType === 'tablet'; + const isLargeTablet = deviceType === 'largeTablet'; + const isTV = deviceType === 'tv'; + const isLargeScreen = isTablet || isLargeTablet || isTV; + + // Enhanced spacing and padding for production sections + const horizontalPadding = useMemo(() => { + switch (deviceType) { + case 'tv': + return 32; + case 'largeTablet': + return 28; + case 'tablet': + return 24; + default: + return 16; // phone + } + }, [deviceType]); + // Optimized state management - reduced state variables const [isContentReady, setIsContentReady] = useState(false); const [showCastModal, setShowCastModal] = useState(false); @@ -965,19 +1005,53 @@ const MetadataScreen: React.FC = () => { {/* Production info row — shown below description and above cast for series */} {shouldLoadSecondaryData && Object.keys(groupedEpisodes).length > 0 && metadata?.networks && metadata.networks.length > 0 && metadata?.description && ( - - Network - + + Network + {metadata.networks.slice(0, 6).map((net) => ( - + {net.logo ? ( ) : ( - {net.name} + {net.name} )} ))} @@ -1001,17 +1075,46 @@ const MetadataScreen: React.FC = () => { metadata?.networks && Array.isArray(metadata.networks) && metadata.networks.some((n: any) => !!n?.logo) && metadata?.description && ( - - Production - + + Production + {metadata.networks .filter((net: any) => !!net?.logo) .slice(0, 6) .map((net: any) => ( - + @@ -1041,29 +1144,38 @@ const MetadataScreen: React.FC = () => { {/* Movie Details section - shown above recommendations for movies when TMDB enrichment is ON */} {shouldLoadSecondaryData && Object.keys(groupedEpisodes).length === 0 && metadata?.movieDetails && ( - - Movie Details + + Movie Details {metadata.movieDetails.tagline && ( - - Tagline - + + Tagline + "{metadata.movieDetails.tagline}" )} {metadata.movieDetails.status && ( - - Status - {metadata.movieDetails.status} + + Status + {metadata.movieDetails.status} )} {metadata.movieDetails.releaseDate && ( - - Release Date - + + Release Date + {new Date(metadata.movieDetails.releaseDate).toLocaleDateString('en-US', { year: 'numeric', month: 'long', @@ -1074,43 +1186,43 @@ const MetadataScreen: React.FC = () => { )} {metadata.movieDetails.runtime && ( - - Runtime - + + Runtime + {Math.floor(metadata.movieDetails.runtime / 60)}h {metadata.movieDetails.runtime % 60}m )} {metadata.movieDetails.budget && metadata.movieDetails.budget > 0 && ( - - Budget - + + Budget + ${metadata.movieDetails.budget.toLocaleString()} )} {metadata.movieDetails.revenue && metadata.movieDetails.revenue > 0 && ( - - Revenue - + + Revenue + ${metadata.movieDetails.revenue.toLocaleString()} )} {metadata.movieDetails.originCountry && metadata.movieDetails.originCountry.length > 0 && ( - - Origin Country - {metadata.movieDetails.originCountry.join(', ')} + + Origin Country + {metadata.movieDetails.originCountry.join(', ')} )} {metadata.movieDetails.originalLanguage && ( - - Original Language - {metadata.movieDetails.originalLanguage.toUpperCase()} + + Original Language + {metadata.movieDetails.originalLanguage.toUpperCase()} )} @@ -1158,20 +1270,29 @@ const MetadataScreen: React.FC = () => { {/* TV Details section - shown after episodes for series when TMDB enrichment is ON */} {shouldLoadSecondaryData && Object.keys(groupedEpisodes).length > 0 && metadata?.tvDetails && ( - - Show Details + + Show Details {metadata.tvDetails.status && ( - - Status - {metadata.tvDetails.status} + + Status + {metadata.tvDetails.status} )} {metadata.tvDetails.firstAirDate && ( - - First Air Date - + + First Air Date + {new Date(metadata.tvDetails.firstAirDate).toLocaleDateString('en-US', { year: 'numeric', month: 'long', @@ -1182,9 +1303,9 @@ const MetadataScreen: React.FC = () => { )} {metadata.tvDetails.lastAirDate && ( - - Last Air Date - + + Last Air Date + {new Date(metadata.tvDetails.lastAirDate).toLocaleDateString('en-US', { year: 'numeric', month: 'long', @@ -1195,46 +1316,46 @@ const MetadataScreen: React.FC = () => { )} {metadata.tvDetails.numberOfSeasons && ( - - Seasons - {metadata.tvDetails.numberOfSeasons} + + Seasons + {metadata.tvDetails.numberOfSeasons} )} {metadata.tvDetails.numberOfEpisodes && ( - - Total Episodes - {metadata.tvDetails.numberOfEpisodes} + + Total Episodes + {metadata.tvDetails.numberOfEpisodes} )} {metadata.tvDetails.episodeRunTime && metadata.tvDetails.episodeRunTime.length > 0 && ( - - Episode Runtime - + + Episode Runtime + {metadata.tvDetails.episodeRunTime.join(' - ')} min )} {metadata.tvDetails.originCountry && metadata.tvDetails.originCountry.length > 0 && ( - - Origin Country - {metadata.tvDetails.originCountry.join(', ')} + + Origin Country + {metadata.tvDetails.originCountry.join(', ')} )} {metadata.tvDetails.originalLanguage && ( - - Original Language - {metadata.tvDetails.originalLanguage.toUpperCase()} + + Original Language + {metadata.tvDetails.originalLanguage.toUpperCase()} )} {metadata.tvDetails.createdBy && metadata.tvDetails.createdBy.length > 0 && ( - - Created By - + + Created By + {metadata.tvDetails.createdBy.map(creator => creator.name).join(', ')} @@ -1400,7 +1521,6 @@ const styles = StyleSheet.create({ marginBottom: 8, }, productionContainer: { - paddingHorizontal: 16, marginTop: 0, marginBottom: 20, },