metadatascreen tablet layout overhaul
This commit is contained in:
parent
18bd6ff3ca
commit
f7c0c670d7
9 changed files with 1679 additions and 349 deletions
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue