metadatascreen tablet layout overhaul

This commit is contained in:
tapframe 2025-10-19 19:46:55 +05:30
parent 18bd6ff3ca
commit f7c0c670d7
9 changed files with 1679 additions and 349 deletions

View file

@ -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<boolean>;
}
// 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<ContinueWatchingRef>((props, re
const [deletingItemId, setDeletingItemId] = useState<string | null>(null);
const longPressTimeoutRef = useRef<NodeJS.Timeout | null>(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<ContinueWatchingRef>((props, re
// Memoized render function for continue watching items
const renderContinueWatchingItem = useCallback(({ item }: { item: ContinueWatchingItem }) => (
<TouchableOpacity
style={[styles.wideContentItem, {
backgroundColor: currentTheme.colors.elevation1,
borderColor: currentTheme.colors.border,
shadowColor: currentTheme.colors.black
}]}
style={[
styles.wideContentItem,
{
backgroundColor: currentTheme.colors.elevation1,
borderColor: currentTheme.colors.border,
shadowColor: currentTheme.colors.black,
width: computedItemWidth,
height: computedItemHeight
}
]}
activeOpacity={0.8}
onPress={() => handleContentPress(item.id, item.type)}
onLongPress={() => handleLongPress(item)}
delayLongPress={800}
>
{/* Poster Image */}
<View style={styles.posterContainer}>
<View style={[
styles.posterContainer,
{
width: isTV ? 100 : isLargeTablet ? 90 : isTablet ? 85 : 80
}
]}>
<FastImage
source={{
uri: item.poster || 'https://via.placeholder.com/300x450',
@ -663,21 +753,42 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
</View>
{/* Content Details */}
<View style={styles.contentDetails}>
<View style={[
styles.contentDetails,
{
padding: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
}
]}>
<View style={styles.titleRow}>
{(() => {
const isUpNext = item.type === 'series' && item.progress === 0;
return (
<View style={styles.titleRow}>
<Text
style={[styles.contentTitle, { color: currentTheme.colors.highEmphasis }]}
style={[
styles.contentTitle,
{
color: currentTheme.colors.highEmphasis,
fontSize: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 17 : 16
}
]}
numberOfLines={1}
>
{item.name}
</Text>
{isUpNext && (
<View style={[styles.progressBadge, { backgroundColor: currentTheme.colors.primary }]}>
<Text style={styles.progressText}>Up Next</Text>
<View style={[
styles.progressBadge,
{
backgroundColor: currentTheme.colors.primary,
paddingHorizontal: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8,
paddingVertical: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 3
}
]}>
<Text style={[
styles.progressText,
{ fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12 }
]}>Up Next</Text>
</View>
)}
</View>
@ -690,12 +801,24 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
if (item.type === 'series' && item.season && item.episode) {
return (
<View style={styles.episodeRow}>
<Text style={[styles.episodeText, { color: currentTheme.colors.mediumEmphasis }]}>
<Text style={[
styles.episodeText,
{
color: currentTheme.colors.mediumEmphasis,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 13
}
]}>
Season {item.season}
</Text>
{item.episodeTitle && (
<Text
style={[styles.episodeTitle, { color: currentTheme.colors.mediumEmphasis }]}
style={[
styles.episodeTitle,
{
color: currentTheme.colors.mediumEmphasis,
fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 13 : 12
}
]}
numberOfLines={1}
>
{item.episodeTitle}
@ -705,7 +828,13 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
);
} else {
return (
<Text style={[styles.yearText, { color: currentTheme.colors.mediumEmphasis }]}>
<Text style={[
styles.yearText,
{
color: currentTheme.colors.mediumEmphasis,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 13
}
]}>
{item.year} {item.type === 'movie' ? 'Movie' : 'Series'}
</Text>
);
@ -715,7 +844,12 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
{/* Progress Bar */}
{item.progress > 0 && (
<View style={styles.wideProgressContainer}>
<View style={styles.wideProgressTrack}>
<View style={[
styles.wideProgressTrack,
{
height: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4
}
]}>
<View
style={[
styles.wideProgressBar,
@ -726,20 +860,26 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
]}
/>
</View>
<Text style={[styles.progressLabel, { color: currentTheme.colors.textMuted }]}>
<Text style={[
styles.progressLabel,
{
color: currentTheme.colors.textMuted,
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 11
}
]}>
{Math.round(item.progress)}% watched
</Text>
</View>
)}
</View>
</TouchableOpacity>
), [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(() => <View style={{ width: 16 }} />, []);
const ItemSeparator = useCallback(() => <View style={{ width: itemSpacing }} />, [itemSpacing]);
// If no continue watching items, don't render anything
if (continueWatchingItems.length === 0) {
@ -751,10 +891,23 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((props, re
style={styles.container}
entering={FadeIn.duration(350)}
>
<View style={styles.header}>
<View style={[styles.header, { paddingHorizontal: horizontalPadding }]}>
<View style={styles.titleContainer}>
<Text style={[styles.title, { color: currentTheme.colors.text }]}>Continue Watching</Text>
<View style={[styles.titleUnderline, { backgroundColor: currentTheme.colors.primary }]} />
<Text style={[
styles.title,
{
color: currentTheme.colors.text,
fontSize: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 26 : 24
}
]}>Continue Watching</Text>
<View style={[
styles.titleUnderline,
{
backgroundColor: currentTheme.colors.primary,
width: isTV ? 50 : isLargeTablet ? 45 : isTablet ? 40 : 40,
height: isTV ? 4 : isLargeTablet ? 3.5 : isTablet ? 3 : 3
}
]} />
</View>
</View>
@ -764,7 +917,13 @@ const ContinueWatchingSection = React.forwardRef<ContinueWatchingRef>((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,
},

View file

@ -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 */}
<View style={styles.contentArea}>
<Text style={[styles.seriesName, { color: currentTheme.colors.white, fontSize: isTablet ? 18 : undefined }]} numberOfLines={1}>
<Text style={[
styles.seriesName,
{
color: currentTheme.colors.white,
fontSize: isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 16
}
]} numberOfLines={1}>
{item.seriesName}
</Text>
<Text style={[styles.episodeTitle, { color: 'rgba(255,255,255,0.9)', fontSize: isTablet ? 16 : undefined }]} numberOfLines={2}>
<Text style={[
styles.episodeTitle,
{
color: 'rgba(255,255,255,0.9)',
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14
}
]} numberOfLines={2}>
{item.title}
</Text>
{item.overview && (
<Text style={[styles.overview, { color: 'rgba(255,255,255,0.8)', fontSize: isTablet ? 13 : undefined }]} numberOfLines={isTablet ? 3 : 2}>
<Text style={[
styles.overview,
{
color: 'rgba(255,255,255,0.8)',
fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 13 : 12
}
]} numberOfLines={isLargeScreen ? 3 : 2}>
{item.overview}
</Text>
)}
<View style={styles.dateContainer}>
<Text style={[styles.episodeInfo, { color: 'rgba(255,255,255,0.7)', fontSize: isTablet ? 13 : undefined }]}>
<Text style={[
styles.episodeInfo,
{
color: 'rgba(255,255,255,0.7)',
fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 13 : 12
}
]}>
S{item.season}:E{item.episode}
</Text>
<MaterialIcons
name="event"
size={isTablet ? 16 : 14}
size={isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14}
color={currentTheme.colors.primary}
/>
<Text style={[styles.releaseDate, { color: currentTheme.colors.primary, fontSize: isTablet ? 14 : undefined }]}>
<Text style={[
styles.releaseDate,
{
color: currentTheme.colors.primary,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 13
}
]}>
{formattedDate}
</Text>
</View>
@ -189,14 +298,43 @@ export const ThisWeekSection = React.memo(() => {
style={styles.container}
entering={FadeIn.duration(350)}
>
<View style={styles.header}>
<View style={[styles.header, { paddingHorizontal: horizontalPadding }]}>
<View style={styles.titleContainer}>
<Text style={[styles.title, { color: currentTheme.colors.text }]}>This Week</Text>
<View style={[styles.titleUnderline, { backgroundColor: currentTheme.colors.primary }]} />
<Text style={[
styles.title,
{
color: currentTheme.colors.text,
fontSize: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 26 : 24
}
]}>This Week</Text>
<View style={[
styles.titleUnderline,
{
backgroundColor: currentTheme.colors.primary,
width: isTV ? 50 : isLargeTablet ? 45 : isTablet ? 40 : 40,
height: isTV ? 4 : isLargeTablet ? 3.5 : isTablet ? 3 : 3
}
]} />
</View>
<TouchableOpacity onPress={handleViewAll} style={styles.viewAllButton}>
<Text style={[styles.viewAllText, { color: currentTheme.colors.textMuted }]}>View All</Text>
<MaterialIcons name="chevron-right" size={20} color={currentTheme.colors.textMuted} />
<TouchableOpacity onPress={handleViewAll} style={[
styles.viewAllButton,
{
paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8,
paddingHorizontal: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 10
}
]}>
<Text style={[
styles.viewAllText,
{
color: currentTheme.colors.textMuted,
fontSize: isTV ? 18 : isLargeTablet ? 16 : isTablet ? 15 : 14
}
]}>View All</Text>
<MaterialIcons
name="chevron-right"
size={isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20}
color={currentTheme.colors.textMuted}
/>
</TouchableOpacity>
</View>
@ -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={() => <View style={{ width: 16 }} />}
ItemSeparatorComponent={() => <View style={{ width: itemSpacing }} />}
/>
</Animated.View>
);
@ -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: {

View file

@ -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<CastSectionProps> = ({
}) => {
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 (
<View style={styles.loadingContainer}>
@ -45,25 +126,52 @@ export const CastSection: React.FC<CastSectionProps> = ({
style={styles.castSection}
entering={FadeIn.duration(300).delay(150)}
>
<View style={styles.sectionHeader}>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>Cast</Text>
<View style={[
styles.sectionHeader,
{ paddingHorizontal: horizontalPadding }
]}>
<Text style={[
styles.sectionTitle,
{
color: currentTheme.colors.highEmphasis,
fontSize: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 18,
marginBottom: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
}
]}>Cast</Text>
</View>
<FlatList
horizontal
data={cast}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.castList}
contentContainerStyle={[
styles.castList,
{ paddingHorizontal: horizontalPadding }
]}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item, index }) => (
<Animated.View
entering={FadeIn.duration(300).delay(50 + index * 30)}
>
<TouchableOpacity
style={styles.castCard}
style={[
styles.castCard,
{
width: castCardWidth,
marginRight: castCardSpacing
}
]}
onPress={() => onSelectCastMember(item)}
activeOpacity={0.7}
>
<View style={styles.castImageContainer}>
<View style={[
styles.castImageContainer,
{
width: castImageSize,
height: castImageSize,
borderRadius: castImageSize / 2,
marginBottom: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8
}
]}>
{item.profile_path ? (
<FastImage
source={{
@ -73,16 +181,43 @@ export const CastSection: React.FC<CastSectionProps> = ({
resizeMode={FastImage.resizeMode.cover}
/>
) : (
<View style={[styles.castImagePlaceholder, { backgroundColor: currentTheme.colors.darkBackground }]}>
<Text style={[styles.placeholderText, { color: currentTheme.colors.textMuted }]}>
<View style={[
styles.castImagePlaceholder,
{
backgroundColor: currentTheme.colors.darkBackground,
borderRadius: castImageSize / 2
}
]}>
<Text style={[
styles.placeholderText,
{
color: currentTheme.colors.textMuted,
fontSize: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 26 : 24
}
]}>
{item.name.split(' ').reduce((prev: string, current: string) => prev + current[0], '').substring(0, 2)}
</Text>
</View>
)}
</View>
<Text style={[styles.castName, { color: currentTheme.colors.text }]} numberOfLines={1}>{item.name}</Text>
<Text style={[
styles.castName,
{
color: currentTheme.colors.text,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14,
width: castCardWidth
}
]} numberOfLines={1}>{item.name}</Text>
{isTmdbEnrichmentEnabled && item.character && (
<Text style={[styles.characterName, { color: currentTheme.colors.textMuted }]} numberOfLines={1}>{item.character}</Text>
<Text style={[
styles.characterName,
{
color: currentTheme.colors.textMuted,
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12,
width: castCardWidth,
marginTop: isTV ? 4 : isLargeTablet ? 3 : isTablet ? 2 : 2
}
]} numberOfLines={1}>{item.character}</Text>
)}
</TouchableOpacity>
</Animated.View>
@ -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: {

View file

@ -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 */}
<View style={styles.traktIconContainer}>
<TraktIcon width={16} height={16} />
<TraktIcon width={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16} height={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16} />
</View>
{/* Header Section - Fixed at top */}
<View style={styles.compactHeader}>
<View style={[
styles.compactHeader,
{
marginBottom: isTV ? 10 : isLargeTablet ? 8 : isTablet ? 8 : 8
}
]}>
<View style={styles.usernameContainer}>
<Text style={[styles.compactUsername, { color: theme.colors.highEmphasis }]}>
<Text style={[
styles.compactUsername,
{
color: theme.colors.highEmphasis,
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 16
}
]}>
{username}
</Text>
{user.vip && (
<View style={styles.miniVipBadge}>
<Text style={styles.miniVipText}>VIP</Text>
<View style={[
styles.miniVipBadge,
{
paddingHorizontal: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4,
paddingVertical: isTV ? 2 : isLargeTablet ? 2 : isTablet ? 1 : 1,
borderRadius: isTV ? 8 : isLargeTablet ? 7 : isTablet ? 6 : 6
}
]}>
<Text style={[
styles.miniVipText,
{
fontSize: isTV ? 11 : isLargeTablet ? 10 : isTablet ? 9 : 9
}
]}>VIP</Text>
</View>
)}
</View>
@ -306,48 +398,107 @@ const CompactCommentCard: React.FC<{
{/* Rating - Show stars */}
{comment.user_stats?.rating && (
<View style={styles.compactRating}>
<View style={[
styles.compactRating,
{
marginBottom: isTV ? 10 : isLargeTablet ? 8 : isTablet ? 8 : 8
}
]}>
{renderCompactStars(comment.user_stats.rating)}
<Text style={[styles.compactRatingText, { color: theme.colors.mediumEmphasis }]}>
<Text style={[
styles.compactRatingText,
{
color: theme.colors.mediumEmphasis,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14
}
]}>
{comment.user_stats.rating}/10
</Text>
</View>
)}
{/* Comment Preview - Flexible area that fills space */}
<View style={[styles.commentContainer, shouldBlurContent ? styles.blurredContent : undefined]}>
<View style={[
styles.commentContainer,
shouldBlurContent ? styles.blurredContent : undefined,
{
marginBottom: isTV ? 10 : isLargeTablet ? 8 : isTablet ? 8 : 8
}
]}>
{shouldBlurContent ? (
<Text style={[styles.compactComment, { color: theme.colors.highEmphasis }]}> This comment contains spoilers. Tap to reveal.</Text>
<Text style={[
styles.compactComment,
{
color: theme.colors.highEmphasis,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14,
lineHeight: isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 18
}
]}> This comment contains spoilers. Tap to reveal.</Text>
) : (
<MarkdownText
text={comment.comment}
theme={theme}
numberOfLines={3}
numberOfLines={isLargeScreen ? 4 : 3}
revealedInlineSpoilers={isSpoilerRevealed}
onSpoilerPress={onSpoilerPress}
textStyle={styles.compactComment}
textStyle={[
styles.compactComment,
{
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14,
lineHeight: isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 18
}
]}
/>
)}
</View>
{/* Meta Info - Fixed at bottom */}
<View style={styles.compactMeta}>
<View style={[
styles.compactMeta,
{
paddingTop: isTV ? 8 : isLargeTablet ? 6 : isTablet ? 6 : 6
}
]}>
<View style={styles.compactBadges}>
{comment.spoiler && (
<Text style={[styles.spoilerMiniText, { color: theme.colors.error }]}>Spoiler</Text>
<Text style={[
styles.spoilerMiniText,
{
color: theme.colors.error,
fontSize: isTV ? 13 : isLargeTablet ? 12 : isTablet ? 11 : 11
}
]}>Spoiler</Text>
)}
</View>
<View style={styles.compactStats}>
<Text style={[styles.compactTime, { color: theme.colors.mediumEmphasis }]}>
<Text style={[
styles.compactTime,
{
color: theme.colors.mediumEmphasis,
fontSize: isTV ? 13 : isLargeTablet ? 12 : isTablet ? 11 : 11
}
]}>
{formatRelativeTime(comment.created_at)}
</Text>
{comment.likes > 0 && (
<Text style={[styles.compactStat, { color: theme.colors.mediumEmphasis }]}>
<Text style={[
styles.compactStat,
{
color: theme.colors.mediumEmphasis,
fontSize: isTV ? 13 : isLargeTablet ? 12 : isTablet ? 12 : 12
}
]}>
👍 {comment.likes}
</Text>
)}
{comment.replies > 0 && (
<Text style={[styles.compactStat, { color: theme.colors.mediumEmphasis }]}>
<Text style={[
styles.compactStat,
{
color: theme.colors.mediumEmphasis,
fontSize: isTV ? 13 : isLargeTablet ? 12 : isTablet ? 12 : 12
}
]}>
💬 {comment.replies}
</Text>
)}
@ -578,6 +729,38 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
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<CommentsSectionProps> = ({
}
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={[styles.title, { color: currentTheme.colors.highEmphasis }]}>
<View style={[
styles.container,
{ paddingHorizontal: horizontalPadding }
]}>
<View style={[
styles.header,
{
marginBottom: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
}
]}>
<Text style={[
styles.title,
{
color: currentTheme.colors.highEmphasis,
fontSize: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 20
}
]}>
Trakt Comments
</Text>
</View>
@ -744,11 +941,14 @@ export const CommentsSection: React.FC<CommentsSectionProps> = ({
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',

View file

@ -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<MetadataDetailsProps> = ({
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 */}
<View style={[styles.metaInfo, loadingMetadata && styles.dimmed]}>
<View style={[
styles.metaInfo,
loadingMetadata && styles.dimmed,
{
paddingHorizontal: horizontalPadding,
gap: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 18
}
]}>
{metadata.year && (
<Text style={[styles.metaText, { color: currentTheme.colors.text }]}>{metadata.year}</Text>
<Text style={[
styles.metaText,
{
color: currentTheme.colors.text,
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15
}
]}>{metadata.year}</Text>
)}
{metadata.runtime && (
<Text style={[styles.metaText, { color: currentTheme.colors.text }]}>
<Text style={[
styles.metaText,
{
color: currentTheme.colors.text,
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15
}
]}>
{formatRuntime(metadata.runtime)}
</Text>
)}
@ -157,17 +218,32 @@ function formatRuntime(runtime: string): string {
<Text style={[
styles.metaText,
styles.premiumOutlinedText,
{ color: getAgeRatingColor(metadata.certification, type === 'series' ? 'series' : 'movie') }
{
color: getAgeRatingColor(metadata.certification, type === 'series' ? 'series' : 'movie'),
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15
}
]}>{metadata.certification}</Text>
)}
{metadata.imdbRating && !isMDBEnabled && (
<View style={styles.ratingContainer}>
<FastImage
source={{ uri: 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/69/IMDB_Logo_2016.svg/575px-IMDB_Logo_2016.svg.png' }}
style={styles.imdbLogo}
style={[
styles.imdbLogo,
{
width: isTV ? 42 : isLargeTablet ? 38 : isTablet ? 35 : 35,
height: isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 18
}
]}
resizeMode={FastImage.resizeMode.contain}
/>
<Text style={[styles.ratingText, { color: currentTheme.colors.text }]}>{metadata.imdbRating}</Text>
<Text style={[
styles.ratingText,
{
color: currentTheme.colors.text,
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15
}
]}>{metadata.imdbRating}</Text>
</View>
)}
</View>
@ -178,18 +254,62 @@ function formatRuntime(runtime: string): string {
{/* Creator/Director Info */}
<Animated.View
entering={FadeIn.duration(300).delay(100)}
style={[styles.creatorContainer, loadingMetadata && styles.dimmed]}
style={[
styles.creatorContainer,
loadingMetadata && styles.dimmed,
{ paddingHorizontal: horizontalPadding }
]}
>
{metadata.directors && metadata.directors.length > 0 && (
<View style={styles.creatorSection}>
<Text style={[styles.creatorLabel, { color: currentTheme.colors.white }]}>Director{metadata.directors.length > 1 ? 's' : ''}:</Text>
<Text style={[styles.creatorText, { color: currentTheme.colors.mediumEmphasis }]}>{metadata.directors.join(', ')}</Text>
<View style={[
styles.creatorSection,
{
height: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20,
marginBottom: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4
}
]}>
<Text style={[
styles.creatorLabel,
{
color: currentTheme.colors.white,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14,
lineHeight: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20
}
]}>Director{metadata.directors.length > 1 ? 's' : ''}:</Text>
<Text style={[
styles.creatorText,
{
color: currentTheme.colors.mediumEmphasis,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14,
lineHeight: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20
}
]}>{metadata.directors.join(', ')}</Text>
</View>
)}
{metadata.creators && metadata.creators.length > 0 && (
<View style={styles.creatorSection}>
<Text style={[styles.creatorLabel, { color: currentTheme.colors.white }]}>Creator{metadata.creators.length > 1 ? 's' : ''}:</Text>
<Text style={[styles.creatorText, { color: currentTheme.colors.mediumEmphasis }]}>{metadata.creators.join(', ')}</Text>
<View style={[
styles.creatorSection,
{
height: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20,
marginBottom: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4
}
]}>
<Text style={[
styles.creatorLabel,
{
color: currentTheme.colors.white,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14,
lineHeight: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20
}
]}>Creator{metadata.creators.length > 1 ? 's' : ''}:</Text>
<Text style={[
styles.creatorText,
{
color: currentTheme.colors.mediumEmphasis,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14,
lineHeight: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20
}
]}>{metadata.creators.join(', ')}</Text>
</View>
)}
</Animated.View>
@ -197,19 +317,41 @@ function formatRuntime(runtime: string): string {
{/* Description */}
{metadata.description && (
<Animated.View
style={[styles.descriptionContainer, loadingMetadata && styles.dimmed]}
style={[
styles.descriptionContainer,
loadingMetadata && styles.dimmed,
{ paddingHorizontal: horizontalPadding }
]}
entering={FadeIn.duration(300)}
>
{/* Hidden text elements to measure heights */}
<Text
style={[styles.description, { color: currentTheme.colors.mediumEmphasis, position: 'absolute', opacity: 0 }]}
style={[
styles.description,
{
color: currentTheme.colors.mediumEmphasis,
position: 'absolute',
opacity: 0,
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15,
lineHeight: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 24
}
]}
numberOfLines={3}
onLayout={handleCollapsedTextLayout}
>
{metadata.description}
</Text>
<Text
style={[styles.description, { color: currentTheme.colors.mediumEmphasis, position: 'absolute', opacity: 0 }]}
style={[
styles.description,
{
color: currentTheme.colors.mediumEmphasis,
position: 'absolute',
opacity: 0,
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15,
lineHeight: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 24
}
]}
onLayout={handleExpandedTextLayout}
>
{metadata.description}
@ -222,7 +364,14 @@ function formatRuntime(runtime: string): string {
>
<Animated.View style={animatedDescriptionStyle}>
<Text
style={[styles.description, { color: currentTheme.colors.mediumEmphasis }]}
style={[
styles.description,
{
color: currentTheme.colors.mediumEmphasis,
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 15,
lineHeight: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 24
}
]}
numberOfLines={isFullDescriptionOpen ? undefined : 3}
onTextLayout={handleTextLayout}
>
@ -230,13 +379,25 @@ function formatRuntime(runtime: string): string {
</Text>
</Animated.View>
{(isTextTruncated || isFullDescriptionOpen) && (
<View style={styles.showMoreButton}>
<Text style={[styles.showMoreText, { color: currentTheme.colors.textMuted }]}>
<View style={[
styles.showMoreButton,
{
marginTop: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8,
paddingVertical: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4
}
]}>
<Text style={[
styles.showMoreText,
{
color: currentTheme.colors.textMuted,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14
}
]}>
{isFullDescriptionOpen ? 'Show Less' : 'Show More'}
</Text>
<MaterialIcons
name={isFullDescriptionOpen ? "keyboard-arrow-up" : "keyboard-arrow-down"}
size={18}
size={isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 18}
color={currentTheme.colors.textMuted}
/>
</View>
@ -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,

View file

@ -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<RatingsSectionProps> = ({ 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<RatingsSectionProps> = ({ imdbId, type })
style={[
styles.container,
{
paddingHorizontal: horizontalPadding,
opacity: fadeAnim,
transform: [{
translateY: fadeAnim.interpolate({
@ -180,22 +233,22 @@ export const RatingsSection: React.FC<RatingsSectionProps> = ({ imdbId, type })
const displayValue = config.transform(parseFloat(value as string));
return (
<View key={source} style={styles.compactRatingItem}>
<View key={source} style={[styles.compactRatingItem, { marginRight: itemSpacing }]}>
{config.isImage ? (
<Image
source={config.icon as any}
style={styles.compactRatingIcon}
style={[styles.compactRatingIcon, { width: iconSize, height: iconSize, marginRight: iconTextGap }]}
resizeMode="contain"
/>
) : (
<View style={styles.compactSvgContainer}>
<View style={[styles.compactSvgContainer, { marginRight: iconTextGap }]}>
{React.createElement(config.icon as any, {
width: 16,
height: 16,
width: iconSize,
height: iconSize,
})}
</View>
)}
<Text style={[styles.compactRatingValue, { color: config.color }]}>
<Text style={[styles.compactRatingValue, { color: config.color, fontSize: textSize }]}>
{displayValue}
</Text>
</View>
@ -210,7 +263,6 @@ const styles = StyleSheet.create({
container: {
marginTop: 2,
marginBottom: 8,
paddingHorizontal: 16,
},
loadingContainer: {
height: 40,

View file

@ -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<SeriesContentProps> = ({
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<SeriesContentProps> = ({
const seasons = Object.keys(groupedEpisodes).map(Number).sort((a, b) => a - b);
return (
<View style={[styles.seasonSelectorWrapper, isTablet && styles.seasonSelectorWrapperTablet]}>
<View style={styles.seasonSelectorHeader}>
<View style={[
styles.seasonSelectorWrapper,
{ paddingHorizontal: horizontalPadding }
]}>
<View style={[
styles.seasonSelectorHeader,
{
marginBottom: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
}
]}>
<Text style={[
styles.seasonSelectorTitle,
isTablet && styles.seasonSelectorTitleTablet,
{ color: currentTheme.colors.highEmphasis }
{
color: currentTheme.colors.highEmphasis,
fontSize: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 18
}
]}>Seasons</Text>
{/* Dropdown Toggle Button */}
@ -360,7 +450,10 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
: 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<SeriesContentProps> = ({
{
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<SeriesContentProps> = ({
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<SeriesContentProps> = ({
<TouchableOpacity
style={[
styles.seasonTextButton,
isTablet && styles.seasonTextButtonTablet,
{
marginRight: seasonButtonSpacing,
width: isTV ? 150 : isLargeTablet ? 140 : isTablet ? 130 : 110,
paddingVertical: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12,
paddingHorizontal: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16,
borderRadius: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
},
selectedSeason === season && styles.selectedSeasonTextButton
]}
onPress={() => onSeasonChange(season)}
@ -448,12 +553,23 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
<TouchableOpacity
style={[
styles.seasonButton,
isTablet && styles.seasonButtonTablet,
{
marginRight: seasonButtonSpacing,
width: seasonPosterWidth
},
selectedSeason === season && [styles.selectedSeasonButton, { borderColor: currentTheme.colors.primary }]
]}
onPress={() => onSeasonChange(season)}
>
<View style={[styles.seasonPosterContainer, isTablet && styles.seasonPosterContainerTablet]}>
<View style={[
styles.seasonPosterContainer,
{
width: seasonPosterWidth,
height: seasonPosterHeight,
borderRadius: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 8,
marginBottom: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8
}
]}>
<FastImage
source={{ uri: seasonPoster }}
style={styles.seasonPoster}
@ -462,8 +578,10 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
{selectedSeason === season && (
<View style={[
styles.selectedSeasonIndicator,
isTablet && styles.selectedSeasonIndicatorTablet,
{ backgroundColor: currentTheme.colors.primary }
{
backgroundColor: currentTheme.colors.primary,
height: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4
}
]} />
)}
@ -471,18 +589,19 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
<Text
style={[
styles.seasonButtonText,
isTablet && styles.seasonButtonTextTablet,
{ color: currentTheme.colors.mediumEmphasis },
{
color: currentTheme.colors.mediumEmphasis,
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 14
},
selectedSeason === season && [
styles.selectedSeasonButtonText,
isTablet && styles.selectedSeasonButtonTextTablet,
{ color: currentTheme.colors.primary }
]
]}
>
Season {season}
</Text>
</TouchableOpacity>
</TouchableOpacity>
</View>
);
}}
@ -550,22 +669,43 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
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}
>
<View style={[
styles.episodeImageContainer,
isTablet && styles.episodeImageContainerTablet
{
width: isTV ? 200 : isLargeTablet ? 180 : isTablet ? 160 : 120,
height: isTV ? 200 : isLargeTablet ? 180 : isTablet ? 160 : 120
}
]}>
<FastImage
source={{ uri: episodeImage }}
style={styles.episodeImage}
resizeMode={FastImage.resizeMode.cover}
/>
<View style={styles.episodeNumberBadge}>
<Text style={styles.episodeNumberText}>{episodeString}</Text>
<View style={[
styles.episodeNumberBadge,
{
paddingHorizontal: isTV ? 8 : isLargeTablet ? 7 : isTablet ? 6 : 6,
paddingVertical: isTV ? 4 : isLargeTablet ? 3 : isTablet ? 2 : 2,
borderRadius: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4
}
]}>
<Text style={[
styles.episodeNumberText,
{
fontSize: isTV ? 13 : isLargeTablet ? 12 : isTablet ? 11 : 11,
fontWeight: '600'
}
]}>{episodeString}</Text>
</View>
{showProgress && (
<View style={styles.progressBarContainer}>
@ -578,53 +718,98 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
</View>
)}
{progressPercent >= 85 && (
<View style={[styles.completedBadge, { backgroundColor: currentTheme.colors.primary }]}>
<MaterialIcons name="check" size={12} color={currentTheme.colors.white} />
<View style={[
styles.completedBadge,
{
backgroundColor: currentTheme.colors.primary,
width: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20,
height: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20,
borderRadius: isTV ? 12 : isLargeTablet ? 11 : isTablet ? 10 : 10
}
]}>
<MaterialIcons name="check" size={isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12} color={currentTheme.colors.white} />
</View>
)}
</View>
<View style={[
styles.episodeInfo,
isTablet && styles.episodeInfoTablet
{
paddingLeft: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 12,
flex: 1,
justifyContent: 'center'
}
]}>
<View style={[
styles.episodeHeader,
isTablet && styles.episodeHeaderTablet
{
marginBottom: isTV ? 8 : isLargeTablet ? 6 : isTablet ? 6 : 4
}
]}>
<Text style={[
styles.episodeTitle,
isTablet && styles.episodeTitleTablet,
{ color: currentTheme.colors.text }
]} numberOfLines={2}>
{
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}
</Text>
<View style={[
styles.episodeMetadata,
isTablet && styles.episodeMetadataTablet
{
gap: isTV ? 8 : isLargeTablet ? 7 : isTablet ? 6 : 4,
flexWrap: 'wrap'
}
]}>
{effectiveVote > 0 && (
<View style={styles.ratingContainer}>
<FastImage
source={{ uri: TMDB_LOGO }}
style={styles.tmdbLogo}
style={[
styles.tmdbLogo,
{
width: isTV ? 22 : isLargeTablet ? 20 : isTablet ? 20 : 20,
height: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14
}
]}
resizeMode={FastImage.resizeMode.contain}
/>
<Text style={[styles.ratingText, { color: currentTheme.colors.textMuted }]}>
<Text style={[
styles.ratingText,
{
color: currentTheme.colors.textMuted,
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 13 : 13
}
]}>
{effectiveVote.toFixed(1)}
</Text>
</View>
)}
{effectiveRuntime && (
<View style={styles.runtimeContainer}>
<MaterialIcons name="schedule" size={14} color={currentTheme.colors.textMuted} />
<Text style={[styles.runtimeText, { color: currentTheme.colors.textMuted }]}>
<MaterialIcons name="schedule" size={isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14} color={currentTheme.colors.textMuted} />
<Text style={[
styles.runtimeText,
{
color: currentTheme.colors.textMuted,
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 13 : 13
}
]}>
{formatRuntime(effectiveRuntime)}
</Text>
</View>
)}
{episode.air_date && (
<Text style={[styles.airDateText, { color: currentTheme.colors.textMuted }]}>
<Text style={[
styles.airDateText,
{
color: currentTheme.colors.textMuted,
fontSize: isTV ? 13 : isLargeTablet ? 12 : isTablet ? 12 : 12
}
]}>
{formatDate(episode.air_date)}
</Text>
)}
@ -632,9 +817,12 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
</View>
<Text style={[
styles.episodeOverview,
isTablet && styles.episodeOverviewTablet,
{ color: currentTheme.colors.mediumEmphasis }
]} numberOfLines={isTablet ? 3 : 2}>
{
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'}
</Text>
</View>
@ -684,16 +872,19 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
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<SeriesContentProps> = ({
style={styles.episodeGradient}
>
{/* Content Container */}
<View style={[styles.episodeContent, isTablet && styles.episodeContentTablet]}>
<View style={[
styles.episodeContent,
{
padding: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 12,
paddingBottom: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 16
}
]}>
{/* Episode Number Badge */}
<View style={[styles.episodeNumberBadgeHorizontal, isTablet && styles.episodeNumberBadgeHorizontalTablet]}>
<Text style={[styles.episodeNumberHorizontal, isTablet && styles.episodeNumberHorizontalTablet]}>{episodeString}</Text>
<View style={[
styles.episodeNumberBadgeHorizontal,
{
paddingHorizontal: isTV ? 10 : isLargeTablet ? 8 : isTablet ? 6 : 6,
paddingVertical: isTV ? 5 : isLargeTablet ? 4 : isTablet ? 3 : 3,
borderRadius: isTV ? 8 : isLargeTablet ? 6 : isTablet ? 4 : 4,
marginBottom: isTV ? 10 : isLargeTablet ? 8 : isTablet ? 6 : 6
}
]}>
<Text style={[
styles.episodeNumberHorizontal,
{
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 10,
fontWeight: isTV ? '700' : isLargeTablet ? '700' : isTablet ? '600' : '600'
}
]}>{episodeString}</Text>
</View>
{/* Episode Title */}
<Text style={[styles.episodeTitleHorizontal, isTablet && styles.episodeTitleHorizontalTablet]} numberOfLines={2}>
<Text style={[
styles.episodeTitleHorizontal,
{
fontSize: isTV ? 20 : isLargeTablet ? 19 : isTablet ? 18 : 15,
fontWeight: isTV ? '800' : isLargeTablet ? '800' : isTablet ? '700' : '700',
lineHeight: isTV ? 26 : isLargeTablet ? 24 : isTablet ? 22 : 18,
marginBottom: isTV ? 8 : isLargeTablet ? 6 : isTablet ? 4 : 4
}
]} numberOfLines={2}>
{episode.name}
</Text>
{/* Episode Description */}
<Text style={[styles.episodeDescriptionHorizontal, isTablet && styles.episodeDescriptionHorizontalTablet]} numberOfLines={3}>
<Text style={[
styles.episodeDescriptionHorizontal,
{
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 12,
lineHeight: isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 16,
marginBottom: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8,
opacity: isTV ? 0.95 : isLargeTablet ? 0.9 : isTablet ? 0.9 : 0.9
}
]} numberOfLines={isLargeScreen ? 4 : 3}>
{episode.overview || 'No description available'}
</Text>
{/* Metadata Row */}
<View style={styles.episodeMetadataRowHorizontal}>
<View style={[
styles.episodeMetadataRowHorizontal,
{
gap: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8
}
]}>
{episode.runtime && (
<View style={styles.runtimeContainerHorizontal}>
<Text style={styles.runtimeTextHorizontal}>
<Text style={[
styles.runtimeTextHorizontal,
{
fontSize: isTV ? 13 : isLargeTablet ? 12 : isTablet ? 11 : 11,
fontWeight: isTV ? '600' : isLargeTablet ? '500' : isTablet ? '500' : '500'
}
]}>
{formatRuntime(episode.runtime)}
</Text>
</View>
)}
{episode.vote_average > 0 && (
<View style={styles.ratingContainerHorizontal}>
<MaterialIcons name="star" size={14} color="#FFD700" />
<Text style={styles.ratingTextHorizontal}>
<MaterialIcons name="star" size={isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 14} color="#FFD700" />
<Text style={[
styles.ratingTextHorizontal,
{
fontSize: isTV ? 13 : isLargeTablet ? 12 : isTablet ? 11 : 11,
fontWeight: isTV ? '600' : isLargeTablet ? '600' : isTablet ? '600' : '600'
}
]}>
{episode.vote_average.toFixed(1)}
</Text>
</View>
@ -799,10 +1043,18 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
{/* Completed Badge */}
{progressPercent >= 85 && (
<View style={[styles.completedBadgeHorizontal, {
backgroundColor: currentTheme.colors.primary,
}]}>
<MaterialIcons name="check" size={16} color="#fff" />
<View style={[
styles.completedBadgeHorizontal,
{
backgroundColor: currentTheme.colors.primary,
width: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 24 : 24,
height: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 24 : 24,
borderRadius: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12,
top: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12,
left: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
}
]}>
<MaterialIcons name="check" size={isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16} color="#fff" />
</View>
)}
@ -824,7 +1076,15 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
<Animated.View
entering={FadeIn.duration(300).delay(100)}
>
<Text style={[styles.sectionTitle, { color: currentTheme.colors.highEmphasis }]}>
<Text style={[
styles.sectionTitle,
{
color: currentTheme.colors.highEmphasis,
fontSize: isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20,
marginBottom: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16,
paddingHorizontal: horizontalPadding
}
]}>
{currentSeasonEpisodes.length} {currentSeasonEpisodes.length === 1 ? 'Episode' : 'Episodes'}
</Text>
@ -854,7 +1114,10 @@ export const SeriesContent: React.FC<SeriesContentProps> = ({
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<SeriesContentProps> = ({
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<SeriesContentProps> = ({
</Animated.View>
)}
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: {

View file

@ -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<TrailersSectionProps> = memo(({
const [dropdownVisible, setDropdownVisible] = useState(false);
const [backendAvailable, setBackendAvailable] = useState<boolean | null>(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<TrailersSectionProps> = memo(({
}
return (
<Animated.View style={[styles.container, sectionAnimatedStyle]}>
<Animated.View style={[
styles.container,
sectionAnimatedStyle,
{ paddingHorizontal: horizontalPadding }
]}>
{/* Enhanced Header with Category Selector */}
<View style={styles.header}>
<Text style={[styles.headerTitle, { color: currentTheme.colors.highEmphasis }]}>
<Text style={[
styles.headerTitle,
{
color: currentTheme.colors.highEmphasis,
fontSize: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 20
}
]}>
Trailers & Videos
</Text>
{/* Category Selector - Right Aligned */}
{trailerCategories.length > 0 && selectedCategory && (
<TouchableOpacity
style={[styles.categorySelector, { borderColor: 'rgba(255,255,255,0.6)' }]}
style={[
styles.categorySelector,
{
borderColor: 'rgba(255,255,255,0.6)',
paddingHorizontal: isTV ? 14 : isLargeTablet ? 12 : isTablet ? 10 : 10,
paddingVertical: isTV ? 8 : isLargeTablet ? 6 : isTablet ? 5 : 5,
borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16,
maxWidth: isTV ? 200 : isLargeTablet ? 180 : isTablet ? 160 : 160
}
]}
onPress={toggleDropdown}
activeOpacity={0.8}
>
<Text
style={[styles.categorySelectorText, { color: currentTheme.colors.highEmphasis }]}
style={[
styles.categorySelectorText,
{
color: currentTheme.colors.highEmphasis,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 12,
maxWidth: isTV ? 150 : isLargeTablet ? 130 : isTablet ? 120 : 120
}
]}
numberOfLines={1}
ellipsizeMode="tail"
>
@ -485,7 +575,7 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
</Text>
<MaterialIcons
name={dropdownVisible ? "expand-less" : "expand-more"}
size={18}
size={isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 18}
color="rgba(255,255,255,0.7)"
/>
</TouchableOpacity>
@ -506,32 +596,58 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
>
<View style={[styles.dropdownContainer, {
backgroundColor: currentTheme.colors.background,
borderColor: currentTheme.colors.primary + '20'
borderColor: currentTheme.colors.primary + '20',
maxWidth: isTV ? 400 : isLargeTablet ? 360 : isTablet ? 320 : 320,
borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
}]}>
{trailerCategories.map(category => (
<TouchableOpacity
key={category}
style={styles.dropdownItem}
style={[
styles.dropdownItem,
{
paddingHorizontal: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16,
paddingVertical: isTV ? 18 : isLargeTablet ? 16 : isTablet ? 14 : 14
}
]}
onPress={() => handleCategorySelect(category)}
activeOpacity={0.7}
>
<View style={styles.dropdownItemContent}>
<View style={[styles.categoryIconContainer, {
backgroundColor: currentTheme.colors.primary + '15'
}]}>
<View style={[
styles.categoryIconContainer,
{
backgroundColor: currentTheme.colors.primary + '15',
width: isTV ? 36 : isLargeTablet ? 32 : isTablet ? 28 : 28,
height: isTV ? 36 : isLargeTablet ? 32 : isTablet ? 28 : 28,
borderRadius: isTV ? 10 : isLargeTablet ? 9 : isTablet ? 8 : 8
}
]}>
<MaterialIcons
name={getTrailerTypeIcon(category) as any}
size={14}
size={isTV ? 18 : isLargeTablet ? 16 : isTablet ? 14 : 14}
color={currentTheme.colors.primary}
/>
</View>
<Text style={[
styles.dropdownItemText,
{ color: currentTheme.colors.highEmphasis }
{
color: currentTheme.colors.highEmphasis,
fontSize: isTV ? 18 : isLargeTablet ? 17 : isTablet ? 16 : 16
}
]}>
{formatTrailerType(category)}
</Text>
<Text style={[styles.dropdownItemCount, { color: currentTheme.colors.textMuted }]}>
<Text style={[
styles.dropdownItemCount,
{
color: currentTheme.colors.textMuted,
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12,
paddingHorizontal: isTV ? 10 : isLargeTablet ? 8 : isTablet ? 8 : 8,
paddingVertical: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4,
borderRadius: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 10 : 10
}
]}>
{trailers[category].length}
</Text>
</View>
@ -548,16 +664,25 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.trailersScrollContent}
contentContainerStyle={[
styles.trailersScrollContent,
{ gap: trailerCardSpacing }
]}
style={styles.trailersScrollView}
decelerationRate="fast"
snapToInterval={isTablet ? 212 : 182} // card width + gap for smooth scrolling
snapToInterval={trailerCardWidth + trailerCardSpacing} // card width + gap for smooth scrolling
snapToAlignment="start"
>
{trailers[selectedCategory].map((trailer, index) => (
<TouchableOpacity
key={trailer.id}
style={styles.trailerCard}
style={[
styles.trailerCard,
{
width: trailerCardWidth,
borderRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
}
]}
onPress={() => handleTrailerPress(trailer)}
activeOpacity={0.9}
>
@ -565,33 +690,71 @@ const TrailersSection: React.FC<TrailersSectionProps> = memo(({
<View style={styles.thumbnailWrapper}>
<FastImage
source={{ uri: getYouTubeThumbnail(trailer.key, 'hq') }}
style={styles.thumbnail}
style={[
styles.thumbnail,
{
borderTopLeftRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16,
borderTopRightRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
}
]}
resizeMode={FastImage.resizeMode.cover}
/>
{/* Subtle Gradient Overlay */}
<View style={styles.thumbnailGradient} />
<View style={[
styles.thumbnailGradient,
{
borderTopLeftRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16,
borderTopRightRadius: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 16 : 16
}
]} />
</View>
{/* Trailer Info */}
<View style={styles.trailerInfo}>
<View style={[
styles.trailerInfo,
{
padding: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
}
]}>
<Text
style={[styles.trailerTitle, { color: currentTheme.colors.highEmphasis }]}
style={[
styles.trailerTitle,
{
color: currentTheme.colors.highEmphasis,
fontSize: isTV ? 16 : isLargeTablet ? 15 : isTablet ? 14 : 12,
lineHeight: isTV ? 22 : isLargeTablet ? 20 : isTablet ? 18 : 16,
marginBottom: isTV ? 6 : isLargeTablet ? 5 : isTablet ? 4 : 4
}
]}
numberOfLines={2}
>
{trailer.displayName || trailer.name}
</Text>
<Text style={[styles.trailerMeta, { color: currentTheme.colors.textMuted }]}>
<Text style={[
styles.trailerMeta,
{
color: currentTheme.colors.textMuted,
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 10
}
]}>
{new Date(trailer.published_at).getFullYear()}
</Text>
</View>
</TouchableOpacity>
))}
{/* Scroll Indicator - shows when there are more items to scroll */}
{trailers[selectedCategory].length > (isTablet ? 4 : 3) && (
<View style={styles.scrollIndicator}>
{trailers[selectedCategory].length > (isTV ? 5 : isLargeTablet ? 4 : isTablet ? 4 : 3) && (
<View style={[
styles.scrollIndicator,
{
width: isTV ? 32 : isLargeTablet ? 28 : isTablet ? 24 : 24,
height: isTV ? 28 : isLargeTablet ? 24 : isTablet ? 20 : 20,
borderRadius: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
}
]}>
<MaterialIcons
name="chevron-right"
size={20}
size={isTV ? 24 : isLargeTablet ? 22 : isTablet ? 20 : 20}
color={currentTheme.colors.textMuted}
style={{ opacity: 0.6 }}
/>
@ -614,7 +777,6 @@ const TrailersSection: React.FC<TrailersSectionProps> = 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,

View file

@ -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 && (
<Animated.View style={[styles.productionContainer, networkSectionAnimatedStyle]}>
<Text style={styles.productionHeader}>Network</Text>
<View style={styles.productionRow}>
<Animated.View style={[
styles.productionContainer,
networkSectionAnimatedStyle,
{ paddingHorizontal: horizontalPadding }
]}>
<Text style={[
styles.productionHeader,
{
fontSize: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 17 : 16,
marginBottom: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
}
]}>Network</Text>
<View style={[
styles.productionRow,
{
gap: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8
}
]}>
{metadata.networks.slice(0, 6).map((net) => (
<View key={String(net.id || net.name)} style={styles.productionChip}>
<View key={String(net.id || net.name)} style={[
styles.productionChip,
{
paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8,
paddingHorizontal: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12,
minHeight: isTV ? 48 : isLargeTablet ? 44 : isTablet ? 40 : 36,
borderRadius: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
}
]}>
{net.logo ? (
<FastImage
source={{ uri: net.logo }}
style={styles.productionLogo}
style={[
styles.productionLogo,
{
width: isTV ? 80 : isLargeTablet ? 72 : isTablet ? 64 : 64,
height: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 22
}
]}
resizeMode={FastImage.resizeMode.contain}
/>
) : (
<Text style={styles.productionText}>{net.name}</Text>
<Text style={[
styles.productionText,
{
fontSize: isTV ? 14 : isLargeTablet ? 13 : isTablet ? 12 : 12
}
]}>{net.name}</Text>
)}
</View>
))}
@ -1001,17 +1075,46 @@ const MetadataScreen: React.FC = () => {
metadata?.networks && Array.isArray(metadata.networks) &&
metadata.networks.some((n: any) => !!n?.logo) &&
metadata?.description && (
<Animated.View style={[styles.productionContainer, productionSectionAnimatedStyle]}>
<Text style={styles.productionHeader}>Production</Text>
<View style={styles.productionRow}>
<Animated.View style={[
styles.productionContainer,
productionSectionAnimatedStyle,
{ paddingHorizontal: horizontalPadding }
]}>
<Text style={[
styles.productionHeader,
{
fontSize: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 17 : 16,
marginBottom: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
}
]}>Production</Text>
<View style={[
styles.productionRow,
{
gap: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8
}
]}>
{metadata.networks
.filter((net: any) => !!net?.logo)
.slice(0, 6)
.map((net: any) => (
<View key={String(net.id || net.name)} style={styles.productionChip}>
<View key={String(net.id || net.name)} style={[
styles.productionChip,
{
paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8,
paddingHorizontal: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12,
minHeight: isTV ? 48 : isLargeTablet ? 44 : isTablet ? 40 : 36,
borderRadius: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
}
]}>
<FastImage
source={{ uri: net.logo }}
style={styles.productionLogo}
style={[
styles.productionLogo,
{
width: isTV ? 80 : isLargeTablet ? 72 : isTablet ? 64 : 64,
height: isTV ? 28 : isLargeTablet ? 26 : isTablet ? 24 : 22
}
]}
resizeMode={FastImage.resizeMode.contain}
/>
</View>
@ -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 && (
<View style={styles.tvDetailsContainer}>
<Text style={styles.tvDetailsHeader}>Movie Details</Text>
<View style={[
styles.tvDetailsContainer,
{ paddingHorizontal: horizontalPadding }
]}>
<Text style={[
styles.tvDetailsHeader,
{
fontSize: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 17 : 16,
marginBottom: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
}
]}>Movie Details</Text>
{metadata.movieDetails.tagline && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Tagline</Text>
<Text style={[styles.tvDetailValue, { fontStyle: 'italic' }]}>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Tagline</Text>
<Text style={[styles.tvDetailValue, { fontStyle: 'italic', fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>
"{metadata.movieDetails.tagline}"
</Text>
</View>
)}
{metadata.movieDetails.status && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Status</Text>
<Text style={styles.tvDetailValue}>{metadata.movieDetails.status}</Text>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Status</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>{metadata.movieDetails.status}</Text>
</View>
)}
{metadata.movieDetails.releaseDate && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Release Date</Text>
<Text style={styles.tvDetailValue}>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Release Date</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>
{new Date(metadata.movieDetails.releaseDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
@ -1074,43 +1186,43 @@ const MetadataScreen: React.FC = () => {
)}
{metadata.movieDetails.runtime && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Runtime</Text>
<Text style={styles.tvDetailValue}>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Runtime</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>
{Math.floor(metadata.movieDetails.runtime / 60)}h {metadata.movieDetails.runtime % 60}m
</Text>
</View>
)}
{metadata.movieDetails.budget && metadata.movieDetails.budget > 0 && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Budget</Text>
<Text style={styles.tvDetailValue}>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Budget</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>
${metadata.movieDetails.budget.toLocaleString()}
</Text>
</View>
)}
{metadata.movieDetails.revenue && metadata.movieDetails.revenue > 0 && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Revenue</Text>
<Text style={styles.tvDetailValue}>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Revenue</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>
${metadata.movieDetails.revenue.toLocaleString()}
</Text>
</View>
)}
{metadata.movieDetails.originCountry && metadata.movieDetails.originCountry.length > 0 && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Origin Country</Text>
<Text style={styles.tvDetailValue}>{metadata.movieDetails.originCountry.join(', ')}</Text>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Origin Country</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>{metadata.movieDetails.originCountry.join(', ')}</Text>
</View>
)}
{metadata.movieDetails.originalLanguage && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Original Language</Text>
<Text style={styles.tvDetailValue}>{metadata.movieDetails.originalLanguage.toUpperCase()}</Text>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Original Language</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>{metadata.movieDetails.originalLanguage.toUpperCase()}</Text>
</View>
)}
</View>
@ -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 && (
<View style={styles.tvDetailsContainer}>
<Text style={styles.tvDetailsHeader}>Show Details</Text>
<View style={[
styles.tvDetailsContainer,
{ paddingHorizontal: horizontalPadding }
]}>
<Text style={[
styles.tvDetailsHeader,
{
fontSize: isTV ? 20 : isLargeTablet ? 18 : isTablet ? 17 : 16,
marginBottom: isTV ? 16 : isLargeTablet ? 14 : isTablet ? 12 : 12
}
]}>Show Details</Text>
{metadata.tvDetails.status && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Status</Text>
<Text style={styles.tvDetailValue}>{metadata.tvDetails.status}</Text>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Status</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>{metadata.tvDetails.status}</Text>
</View>
)}
{metadata.tvDetails.firstAirDate && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>First Air Date</Text>
<Text style={styles.tvDetailValue}>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>First Air Date</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>
{new Date(metadata.tvDetails.firstAirDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
@ -1182,9 +1303,9 @@ const MetadataScreen: React.FC = () => {
)}
{metadata.tvDetails.lastAirDate && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Last Air Date</Text>
<Text style={styles.tvDetailValue}>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Last Air Date</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>
{new Date(metadata.tvDetails.lastAirDate).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
@ -1195,46 +1316,46 @@ const MetadataScreen: React.FC = () => {
)}
{metadata.tvDetails.numberOfSeasons && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Seasons</Text>
<Text style={styles.tvDetailValue}>{metadata.tvDetails.numberOfSeasons}</Text>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Seasons</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>{metadata.tvDetails.numberOfSeasons}</Text>
</View>
)}
{metadata.tvDetails.numberOfEpisodes && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Total Episodes</Text>
<Text style={styles.tvDetailValue}>{metadata.tvDetails.numberOfEpisodes}</Text>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Total Episodes</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>{metadata.tvDetails.numberOfEpisodes}</Text>
</View>
)}
{metadata.tvDetails.episodeRunTime && metadata.tvDetails.episodeRunTime.length > 0 && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Episode Runtime</Text>
<Text style={styles.tvDetailValue}>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Episode Runtime</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>
{metadata.tvDetails.episodeRunTime.join(' - ')} min
</Text>
</View>
)}
{metadata.tvDetails.originCountry && metadata.tvDetails.originCountry.length > 0 && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Origin Country</Text>
<Text style={styles.tvDetailValue}>{metadata.tvDetails.originCountry.join(', ')}</Text>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Origin Country</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>{metadata.tvDetails.originCountry.join(', ')}</Text>
</View>
)}
{metadata.tvDetails.originalLanguage && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Original Language</Text>
<Text style={styles.tvDetailValue}>{metadata.tvDetails.originalLanguage.toUpperCase()}</Text>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Original Language</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>{metadata.tvDetails.originalLanguage.toUpperCase()}</Text>
</View>
)}
{metadata.tvDetails.createdBy && metadata.tvDetails.createdBy.length > 0 && (
<View style={styles.tvDetailRow}>
<Text style={styles.tvDetailLabel}>Created By</Text>
<Text style={styles.tvDetailValue}>
<View style={[styles.tvDetailRow, { paddingVertical: isTV ? 12 : isLargeTablet ? 10 : isTablet ? 8 : 8 }]}>
<Text style={[styles.tvDetailLabel, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>Created By</Text>
<Text style={[styles.tvDetailValue, { fontSize: isTV ? 15 : isLargeTablet ? 14 : isTablet ? 14 : 14 }]}>
{metadata.tvDetails.createdBy.map(creator => creator.name).join(', ')}
</Text>
</View>
@ -1400,7 +1521,6 @@ const styles = StyleSheet.create({
marginBottom: 8,
},
productionContainer: {
paddingHorizontal: 16,
marginTop: 0,
marginBottom: 20,
},