import React, { useEffect, useState, useRef } from 'react'; import { View, Text, StyleSheet, ScrollView, TouchableOpacity, ActivityIndicator, Dimensions, useWindowDimensions, useColorScheme, FlatList } from 'react-native'; import { Image } from 'expo-image'; import { MaterialIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { FlashList, FlashListRef } from '@shopify/flash-list'; import { useTheme } from '../../contexts/ThemeContext'; import { useSettings } from '../../hooks/useSettings'; import { Episode } from '../../types/metadata'; import { tmdbService } from '../../services/tmdbService'; import { storageService } from '../../services/storageService'; import { useFocusEffect } from '@react-navigation/native'; import Animated, { FadeIn, FadeOut, SlideInRight, SlideOutLeft } from 'react-native-reanimated'; import { TraktService } from '../../services/traktService'; import { logger } from '../../utils/logger'; import AsyncStorage from '@react-native-async-storage/async-storage'; interface SeriesContentProps { episodes: Episode[]; selectedSeason: number; loadingSeasons: boolean; onSeasonChange: (season: number) => void; onSelectEpisode: (episode: Episode) => void; groupedEpisodes?: { [seasonNumber: number]: Episode[] }; metadata?: { poster?: string; id?: string }; } // Add placeholder constant at the top const DEFAULT_PLACEHOLDER = 'https://via.placeholder.com/300x450/1a1a1a/666666?text=No+Image'; const EPISODE_PLACEHOLDER = 'https://via.placeholder.com/500x280/1a1a1a/666666?text=No+Preview'; const TMDB_LOGO = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/Tmdb.new.logo.svg/512px-Tmdb.new.logo.svg.png?20200406190906'; export const SeriesContent: React.FC = ({ episodes, selectedSeason, loadingSeasons, onSeasonChange, onSelectEpisode, groupedEpisodes = {}, metadata }) => { const { currentTheme } = useTheme(); const { settings } = useSettings(); const { width } = useWindowDimensions(); const isTablet = width > 768; const isDarkMode = useColorScheme() === 'dark'; 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); // Local TMDB hydration for rating/runtime when addon (Cinemeta) lacks these const [tmdbEpisodeOverrides, setTmdbEpisodeOverrides] = useState<{ [epKey: string]: { vote_average?: number; runtime?: number; still_path?: string } }>({}); // Add state for season view mode (persists for current show across navigation) const [seasonViewMode, setSeasonViewMode] = useState<'posters' | 'text'>('posters'); // View mode state (no animations) const [posterViewVisible, setPosterViewVisible] = useState(true); const [textViewVisible, setTextViewVisible] = useState(false); // Add refs for the scroll views const seasonScrollViewRef = useRef(null); const episodeScrollViewRef = useRef>(null); const horizontalEpisodeScrollViewRef = useRef>(null); // Load saved view mode preference when component mounts or show changes useEffect(() => { const loadViewModePreference = async () => { if (metadata?.id) { try { const savedMode = await AsyncStorage.getItem(`season_view_mode_${metadata.id}`); if (savedMode === 'text' || savedMode === 'posters') { setSeasonViewMode(savedMode); if (__DEV__) console.log('[SeriesContent] Loaded saved view mode:', savedMode, 'for show:', metadata.id); } } catch (error) { if (__DEV__) console.log('[SeriesContent] Error loading view mode preference:', error); } } }; loadViewModePreference(); }, [metadata?.id]); // Initialize view mode visibility based on current view mode useEffect(() => { if (seasonViewMode === 'text') { setPosterViewVisible(false); setTextViewVisible(true); } else { setPosterViewVisible(true); setTextViewVisible(false); } }, [seasonViewMode]); // Update view mode without animations const updateViewMode = (newMode: 'posters' | 'text') => { setSeasonViewMode(newMode); if (metadata?.id) { AsyncStorage.setItem(`season_view_mode_${metadata.id}`, newMode).catch(error => { if (__DEV__) console.log('[SeriesContent] Error saving view mode preference:', error); }); } }; // Add refs for the scroll views const loadEpisodesProgress = async () => { if (!metadata?.id) return; const allProgress = await storageService.getAllWatchProgress(); const progress: { [key: string]: { currentTime: number; duration: number; lastUpdated: number } } = {}; episodes.forEach(episode => { const episodeId = episode.stremioId || `${metadata.id}:${episode.season_number}:${episode.episode_number}`; const key = `series:${metadata.id}:${episodeId}`; if (allProgress[key]) { progress[episodeId] = { currentTime: allProgress[key].currentTime, duration: allProgress[key].duration, lastUpdated: allProgress[key].lastUpdated }; } }); // ---------------- Trakt watched-history integration ---------------- try { const traktService = TraktService.getInstance(); const isAuthed = await traktService.isAuthenticated(); if (isAuthed && metadata?.id) { const historyItems = await traktService.getWatchedEpisodesHistory(1, 400); historyItems.forEach(item => { if (item.type !== 'episode') return; const showImdb = item.show?.ids?.imdb ? `tt${item.show.ids.imdb.replace(/^tt/, '')}` : null; if (!showImdb || showImdb !== metadata.id) return; const season = item.episode?.season; const epNum = item.episode?.number; if (season === undefined || epNum === undefined) return; const episodeId = `${metadata.id}:${season}:${epNum}`; const watchedAt = new Date(item.watched_at).getTime(); // Mark as 100% completed (use 1/1 to avoid divide-by-zero) const traktProgressEntry = { currentTime: 1, duration: 1, lastUpdated: watchedAt, }; const existing = progress[episodeId]; const existingPercent = existing ? (existing.currentTime / existing.duration) * 100 : 0; // Prefer local progress if it is already >=85%; otherwise use Trakt data if (!existing || existingPercent < 85) { progress[episodeId] = traktProgressEntry; } }); } } catch (err) { logger.error('[SeriesContent] Failed to merge Trakt history:', err); } setEpisodeProgress(progress); }; // Function to find and scroll to the most recently watched episode const scrollToMostRecentEpisode = () => { if (!metadata?.id || !settings?.episodeLayoutStyle || settings.episodeLayoutStyle !== 'horizontal') { return; } const currentSeasonEpisodes = groupedEpisodes[selectedSeason] || []; if (currentSeasonEpisodes.length === 0) { return; } // Find the most recently watched episode in the current season let mostRecentEpisodeIndex = -1; let mostRecentTimestamp = 0; let mostRecentEpisodeName = ''; currentSeasonEpisodes.forEach((episode, index) => { const episodeId = episode.stremioId || `${metadata.id}:${episode.season_number}:${episode.episode_number}`; const progress = episodeProgress[episodeId]; if (progress && progress.lastUpdated > mostRecentTimestamp && progress.currentTime > 0) { mostRecentTimestamp = progress.lastUpdated; mostRecentEpisodeIndex = index; mostRecentEpisodeName = episode.name; } }); // Scroll to the most recently watched episode if found if (mostRecentEpisodeIndex >= 0) { const cardWidth = isTablet ? width * 0.4 + 16 : width * 0.85 + 16; const scrollPosition = mostRecentEpisodeIndex * cardWidth; setTimeout(() => { if (horizontalEpisodeScrollViewRef.current) { horizontalEpisodeScrollViewRef.current.scrollToOffset({ offset: scrollPosition, animated: true }); } }, 500); // Delay to ensure the season has loaded } }; // Initial load of watch progress useEffect(() => { loadEpisodesProgress(); }, [episodes, metadata?.id]); // Hydrate TMDB rating/runtime for current season episodes if missing useEffect(() => { const hydrateFromTmdb = async () => { try { if (!metadata?.id || !selectedSeason) return; const currentSeasonEpisodes = groupedEpisodes[selectedSeason] || []; if (currentSeasonEpisodes.length === 0) return; // Check if hydration is needed const needsHydration = currentSeasonEpisodes.some(ep => !(ep as any).runtime || !(ep as any).vote_average); if (!needsHydration) return; // Resolve TMDB show id let tmdbShowId: number | null = null; if (metadata.id.startsWith('tmdb:')) { tmdbShowId = parseInt(metadata.id.split(':')[1], 10); } else if (metadata.id.startsWith('tt')) { tmdbShowId = await tmdbService.findTMDBIdByIMDB(metadata.id); } if (!tmdbShowId) return; // Fetch all episodes from TMDB and build override map for the current season const all = await tmdbService.getAllEpisodes(tmdbShowId); const overrides: { [k: string]: { vote_average?: number; runtime?: number; still_path?: string } } = {}; const seasonEpisodes = all?.[selectedSeason] || []; seasonEpisodes.forEach((tmdbEp: any) => { const key = `${metadata.id}:${tmdbEp.season_number}:${tmdbEp.episode_number}`; overrides[key] = { vote_average: tmdbEp.vote_average, runtime: tmdbEp.runtime, still_path: tmdbEp.still_path, }; }); if (Object.keys(overrides).length > 0) { setTmdbEpisodeOverrides(prev => ({ ...prev, ...overrides })); } } catch (err) { logger.error('[SeriesContent] TMDB hydration failed:', err); } }; hydrateFromTmdb(); }, [metadata?.id, selectedSeason, groupedEpisodes]); // Enable item animations shortly after mount to avoid initial overlap/glitch useEffect(() => { const timer = setTimeout(() => setEnableItemAnimations(true), 200); return () => clearTimeout(timer); }, []); // Refresh watch progress when screen comes into focus useFocusEffect( React.useCallback(() => { loadEpisodesProgress(); }, [episodes, metadata?.id]) ); // Add effect to scroll to selected season useEffect(() => { if (selectedSeason && seasonScrollViewRef.current && Object.keys(groupedEpisodes).length > 0) { // Find the index of the selected season const seasons = Object.keys(groupedEpisodes).map(Number).sort((a, b) => a - b); const selectedIndex = seasons.findIndex(season => season === selectedSeason); if (selectedIndex !== -1) { // Wait a small amount of time for layout to be ready setTimeout(() => { if (seasonScrollViewRef.current && typeof (seasonScrollViewRef.current as any).scrollToOffset === 'function') { (seasonScrollViewRef.current as any).scrollToOffset({ offset: selectedIndex * 116, // 100px width + 16px margin animated: true }); } }, 300); } } }, [selectedSeason, groupedEpisodes]); // Add effect to scroll to most recently watched episode when season changes or progress loads useEffect(() => { if (Object.keys(episodeProgress).length > 0 && selectedSeason && settings?.episodeLayoutStyle) { scrollToMostRecentEpisode(); } }, [selectedSeason, episodeProgress, settings?.episodeLayoutStyle, groupedEpisodes]); if (loadingSeasons) { return ( Loading episodes... ); } if (episodes.length === 0) { return ( No episodes available ); } const renderSeasonSelector = () => { // Show selector if we have grouped episodes data or can derive from episodes if (!groupedEpisodes || Object.keys(groupedEpisodes).length <= 1) { return null; } if (__DEV__) console.log('[SeriesContent] renderSeasonSelector called, current view mode:', seasonViewMode); const seasons = Object.keys(groupedEpisodes).map(Number).sort((a, b) => a - b); return ( Seasons {/* Dropdown Toggle Button */} { const newMode = seasonViewMode === 'posters' ? 'text' : 'posters'; updateViewMode(newMode); if (__DEV__) console.log('[SeriesContent] View mode changed to:', newMode, 'Current ref value:', seasonViewMode); }} activeOpacity={0.7} > {seasonViewMode === 'posters' ? 'Posters' : 'Text'} >} data={seasons} horizontal showsHorizontalScrollIndicator={false} style={styles.seasonSelectorContainer} contentContainerStyle={[styles.seasonSelectorContent, isTablet && styles.seasonSelectorContentTablet]} initialNumToRender={5} maxToRenderPerBatch={5} windowSize={3} renderItem={({ item: season }) => { const seasonEpisodes = groupedEpisodes[season] || []; // Get season poster URL (needed for both views) let seasonPoster = DEFAULT_PLACEHOLDER; if (seasonEpisodes[0]?.season_poster_path) { const tmdbUrl = tmdbService.getImageUrl(seasonEpisodes[0].season_poster_path, 'w500'); if (tmdbUrl) seasonPoster = tmdbUrl; } else if (metadata?.poster) { seasonPoster = metadata.poster; } if (seasonViewMode === 'text') { // Text-only view if (__DEV__) console.log('[SeriesContent] Rendering text view for season:', season, 'View mode ref:', seasonViewMode); return ( onSeasonChange(season)} > Season {season} ); } // Poster view (current implementation) if (__DEV__) console.log('[SeriesContent] Rendering poster view for season:', season, 'View mode ref:', seasonViewMode); return ( onSeasonChange(season)} > {selectedSeason === season && ( )} Season {season} ); }} keyExtractor={season => season.toString()} /> ); }; // Vertical layout episode card (traditional) const renderVerticalEpisodeCard = (episode: Episode) => { let episodeImage = EPISODE_PLACEHOLDER; if (episode.still_path) { // Check if still_path is already a full URL if (episode.still_path.startsWith('http')) { episodeImage = episode.still_path; } else { const tmdbUrl = tmdbService.getImageUrl(episode.still_path, 'w500'); if (tmdbUrl) episodeImage = tmdbUrl; } } else if (metadata?.poster) { episodeImage = metadata.poster; } const episodeNumber = typeof episode.episode_number === 'number' ? episode.episode_number.toString() : ''; const seasonNumber = typeof episode.season_number === 'number' ? episode.season_number.toString() : ''; const episodeString = seasonNumber && episodeNumber ? `S${seasonNumber.padStart(2, '0')}E${episodeNumber.padStart(2, '0')}` : ''; const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }; const formatRuntime = (runtime: number) => { if (!runtime) return null; const hours = Math.floor(runtime / 60); const minutes = runtime % 60; if (hours > 0) { return `${hours}h ${minutes}m`; } return `${minutes}m`; }; // Get episode progress const episodeId = episode.stremioId || `${metadata?.id}:${episode.season_number}:${episode.episode_number}`; const tmdbOverride = tmdbEpisodeOverrides[`${metadata?.id}:${episode.season_number}:${episode.episode_number}`]; const effectiveVote = (tmdbOverride?.vote_average ?? episode.vote_average) || 0; const effectiveRuntime = tmdbOverride?.runtime ?? (episode as any).runtime; if (!episode.still_path && tmdbOverride?.still_path) { const tmdbUrl = tmdbService.getImageUrl(tmdbOverride.still_path, 'w500'); if (tmdbUrl) episodeImage = tmdbUrl; } const progress = episodeProgress[episodeId]; const progressPercent = progress ? (progress.currentTime / progress.duration) * 100 : 0; // Don't show progress bar if episode is complete (>= 85%) const showProgress = progress && progressPercent < 85; return ( onSelectEpisode(episode)} activeOpacity={0.7} > {episodeString} {showProgress && ( )} {progressPercent >= 85 && ( )} {episode.name} {effectiveVote > 0 && ( {effectiveVote.toFixed(1)} )} {effectiveRuntime && ( {formatRuntime(effectiveRuntime)} )} {episode.air_date && ( {formatDate(episode.air_date)} )} {episode.overview || 'No description available'} ); }; // Horizontal layout episode card (Netflix-style) const renderHorizontalEpisodeCard = (episode: Episode) => { let episodeImage = EPISODE_PLACEHOLDER; if (episode.still_path) { // Check if still_path is already a full URL if (episode.still_path.startsWith('http')) { episodeImage = episode.still_path; } else { const tmdbUrl = tmdbService.getImageUrl(episode.still_path, 'w500'); if (tmdbUrl) episodeImage = tmdbUrl; } } else if (metadata?.poster) { episodeImage = metadata.poster; } const episodeNumber = typeof episode.episode_number === 'number' ? episode.episode_number.toString() : ''; const seasonNumber = typeof episode.season_number === 'number' ? episode.season_number.toString() : ''; const episodeString = seasonNumber && episodeNumber ? `EPISODE ${episodeNumber}` : ''; const formatRuntime = (runtime: number) => { if (!runtime) return null; const hours = Math.floor(runtime / 60); const minutes = runtime % 60; if (hours > 0) { return `${hours}h ${minutes}m`; } return `${minutes}m`; }; // Get episode progress const episodeId = episode.stremioId || `${metadata?.id}:${episode.season_number}:${episode.episode_number}`; const progress = episodeProgress[episodeId]; const progressPercent = progress ? (progress.currentTime / progress.duration) * 100 : 0; // Don't show progress bar if episode is complete (>= 85%) const showProgress = progress && progressPercent < 85; return ( onSelectEpisode(episode)} activeOpacity={0.85} > {/* Gradient Border Container */} {/* Background Image */} {/* Standard Gradient Overlay */} {/* Content Container */} {/* Episode Number Badge */} {episodeString} {/* Episode Title */} {episode.name} {/* Episode Description */} {episode.overview || 'No description available'} {/* Metadata Row */} {episode.runtime && ( {formatRuntime(episode.runtime)} )} {episode.vote_average > 0 && ( {episode.vote_average.toFixed(1)} )} {/* Progress Bar */} {showProgress && ( )} {/* Completed Badge */} {progressPercent >= 85 && ( )} ); }; const currentSeasonEpisodes = groupedEpisodes[selectedSeason] || []; return ( {renderSeasonSelector()} {currentSeasonEpisodes.length} {currentSeasonEpisodes.length === 1 ? 'Episode' : 'Episodes'} {/* Show message when no episodes are available for selected season */} {currentSeasonEpisodes.length === 0 && ( No episodes available for Season {selectedSeason} Episodes may not be released yet )} {/* Only render episode list if there are episodes */} {currentSeasonEpisodes.length > 0 && ( (settings?.episodeLayoutStyle === 'horizontal') ? ( // Horizontal Layout (Netflix-style) - Using FlatList ( {renderHorizontalEpisodeCard(episode)} )} keyExtractor={episode => episode.id.toString()} horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={isTablet ? styles.episodeListContentHorizontalTablet : styles.episodeListContentHorizontal} removeClippedSubviews initialNumToRender={3} maxToRenderPerBatch={5} windowSize={5} getItemLayout={(data, index) => { const cardWidth = isTablet ? width * 0.4 : width * 0.75; const margin = isTablet ? 20 : 16; return { length: cardWidth + margin, offset: (cardWidth + margin) * index, index, }; }} /> ) : ( // Vertical Layout (Traditional) - Using FlashList ( {renderVerticalEpisodeCard(episode)} )} keyExtractor={episode => episode.id.toString()} contentContainerStyle={isTablet ? styles.episodeListContentVerticalTablet : styles.episodeListContentVertical} removeClippedSubviews /> ) )} ); }; const styles = StyleSheet.create({ container: { flex: 1, paddingVertical: 16, }, centeredContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20, }, centeredText: { marginTop: 12, fontSize: 16, textAlign: 'center', }, centeredSubText: { marginTop: 8, fontSize: 14, textAlign: 'center', opacity: 0.8, }, sectionTitle: { fontSize: 20, fontWeight: '700', marginBottom: 16, paddingHorizontal: 16, }, episodeList: { flex: 1, }, // Vertical Layout Styles episodeListContentVertical: { paddingBottom: 20, paddingHorizontal: 16, }, episodeListContentVerticalTablet: { paddingHorizontal: 16, paddingBottom: 20, }, episodeGridVertical: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', gap: 16, }, episodeCardVertical: { flexDirection: 'row', borderRadius: 16, marginBottom: 16, overflow: 'hidden', elevation: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.25, shadowRadius: 8, borderWidth: 1, borderColor: 'rgba(255,255,255,0.1)', height: 120, }, episodeCardVerticalTablet: { width: '100%', flexDirection: 'row', height: 160, marginBottom: 16, }, episodeImageContainer: { position: 'relative', width: 120, height: 120, }, episodeImageContainerTablet: { width: 160, height: 160, }, episodeImage: { width: '100%', height: '100%', transform: [{ scale: 1.02 }], }, episodeNumberBadge: { position: 'absolute', bottom: 8, right: 4, backgroundColor: 'rgba(0,0,0,0.85)', paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4, borderWidth: 1, borderColor: 'rgba(255,255,255,0.2)', zIndex: 1, }, episodeNumberText: { color: '#fff', fontSize: 11, fontWeight: '600', letterSpacing: 0.3, }, episodeInfo: { flex: 1, padding: 12, justifyContent: 'center', }, episodeInfoTablet: { padding: 16, }, episodeHeader: { marginBottom: 4, }, episodeHeaderTablet: { marginBottom: 6, }, episodeTitle: { fontSize: 15, fontWeight: '700', letterSpacing: 0.3, marginBottom: 2, }, episodeTitleTablet: { fontSize: 16, marginBottom: 4, }, episodeMetadata: { flexDirection: 'row', alignItems: 'center', gap: 4, }, episodeMetadataTablet: { gap: 6, flexWrap: 'wrap', }, ratingContainer: { flexDirection: 'row', alignItems: 'center', // chip background removed }, tmdbLogo: { width: 20, height: 14, }, ratingText: { color: '#01b4e4', fontSize: 13, fontWeight: '700', marginLeft: 4, }, runtimeContainer: { flexDirection: 'row', alignItems: 'center', // chip background removed }, runtimeText: { fontSize: 13, fontWeight: '600', marginLeft: 4, }, airDateText: { fontSize: 12, opacity: 0.8, }, episodeOverview: { fontSize: 13, lineHeight: 18, }, episodeOverviewTablet: { fontSize: 14, lineHeight: 20, }, progressBarContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 3, backgroundColor: 'rgba(0,0,0,0.5)', }, progressBar: { height: '100%', }, completedBadge: { position: 'absolute', top: 8, left: 8, width: 20, height: 20, borderRadius: 10, alignItems: 'center', justifyContent: 'center', borderWidth: 1, borderColor: 'rgba(255,255,255,0.3)', zIndex: 2, }, // Horizontal Layout Styles episodeListContentHorizontal: { paddingLeft: 16, paddingRight: 16, }, episodeListContentHorizontalTablet: { paddingLeft: 24, paddingRight: 24, }, episodeCardWrapperHorizontal: { width: Dimensions.get('window').width * 0.75, marginRight: 16, }, episodeCardWrapperHorizontalTablet: { width: Dimensions.get('window').width * 0.4, marginRight: 20, }, episodeCardHorizontal: { borderRadius: 16, overflow: 'hidden', elevation: 8, shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.35, shadowRadius: 12, borderWidth: 1, borderColor: 'rgba(255,255,255,0.05)', height: 200, position: 'relative', width: '100%', backgroundColor: 'transparent', }, episodeCardHorizontalTablet: { height: 260, borderRadius: 20, elevation: 12, shadowOpacity: 0.4, shadowRadius: 16, }, episodeBackgroundImage: { width: '100%', height: '100%', borderRadius: 16, }, episodeGradient: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, borderRadius: 16, justifyContent: 'flex-end', }, episodeContent: { padding: 12, paddingBottom: 16, }, episodeContentTablet: { padding: 16, paddingBottom: 20, }, episodeNumberBadgeHorizontal: { backgroundColor: 'rgba(0,0,0,0.4)', paddingHorizontal: 6, paddingVertical: 3, borderRadius: 4, marginBottom: 6, alignSelf: 'flex-start', }, episodeNumberBadgeHorizontalTablet: { backgroundColor: 'rgba(0,0,0,0.5)', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 6, marginBottom: 8, alignSelf: 'flex-start', }, episodeNumberHorizontal: { color: 'rgba(255,255,255,0.8)', fontSize: 10, fontWeight: '600', letterSpacing: 0.8, textTransform: 'uppercase', marginBottom: 2, }, episodeNumberHorizontalTablet: { color: 'rgba(255,255,255,0.9)', fontSize: 12, fontWeight: '700', letterSpacing: 1.0, textTransform: 'uppercase', marginBottom: 2, }, episodeTitleHorizontal: { color: '#fff', fontSize: 15, fontWeight: '700', letterSpacing: -0.3, marginBottom: 4, lineHeight: 18, }, episodeTitleHorizontalTablet: { color: '#fff', fontSize: 18, fontWeight: '800', letterSpacing: -0.4, marginBottom: 6, lineHeight: 22, }, episodeDescriptionHorizontal: { color: 'rgba(255,255,255,0.85)', fontSize: 12, lineHeight: 16, marginBottom: 8, opacity: 0.9, }, episodeDescriptionHorizontalTablet: { color: 'rgba(255,255,255,0.9)', fontSize: 14, lineHeight: 18, marginBottom: 10, opacity: 0.95, }, episodeMetadataRowHorizontal: { flexDirection: 'row', alignItems: 'center', gap: 8, }, runtimeContainerHorizontal: { flexDirection: 'row', alignItems: 'center', // chip background removed }, runtimeTextHorizontal: { color: 'rgba(255,255,255,0.8)', fontSize: 11, fontWeight: '500', }, ratingContainerHorizontal: { flexDirection: 'row', alignItems: 'center', // chip background removed gap: 2, }, ratingTextHorizontal: { color: '#FFD700', fontSize: 11, fontWeight: '600', }, progressBarContainerHorizontal: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 3, backgroundColor: 'rgba(255,255,255,0.2)', }, progressBarHorizontal: { height: '100%', borderRadius: 2, }, completedBadgeHorizontal: { position: 'absolute', top: 12, left: 12, width: 24, height: 24, borderRadius: 12, alignItems: 'center', justifyContent: 'center', borderWidth: 2, borderColor: '#fff', }, // Season Selector Styles seasonSelectorWrapper: { marginBottom: 20, paddingHorizontal: 16, }, seasonSelectorWrapperTablet: { marginBottom: 24, paddingHorizontal: 24, }, seasonSelectorHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, }, seasonSelectorTitle: { fontSize: 18, fontWeight: '600', marginBottom: 0, // Removed margin bottom here }, seasonSelectorTitleTablet: { fontSize: 22, fontWeight: '700', marginBottom: 0, // Removed margin bottom here }, seasonSelectorContainer: { flexGrow: 0, }, seasonSelectorContent: { paddingBottom: 8, }, seasonSelectorContentTablet: { paddingBottom: 12, }, 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%', height: '100%', }, selectedSeasonIndicator: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 4, }, selectedSeasonIndicatorTablet: { position: 'absolute', bottom: 0, left: 0, right: 0, height: 6, }, seasonButtonText: { fontSize: 14, fontWeight: '500', }, seasonButtonTextTablet: { fontSize: 16, fontWeight: '600', }, selectedSeasonButtonText: { fontWeight: '700', }, selectedSeasonButtonTextTablet: { fontWeight: '800', }, seasonViewToggle: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 6, borderWidth: 1, borderColor: 'rgba(255,255,255,0.2)', }, seasonViewToggleText: { fontSize: 12, fontWeight: '500', marginRight: 4, }, 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: { backgroundColor: 'rgba(255,255,255,0.08)', }, seasonTextButtonText: { fontSize: 15, fontWeight: '600', letterSpacing: 0.3, textAlign: 'center', }, seasonTextButtonTextTablet: { fontSize: 17, fontWeight: '600', letterSpacing: 0.4, textAlign: 'center', }, selectedSeasonTextButtonText: { fontWeight: '700', }, selectedSeasonTextButtonTextTablet: { fontWeight: '800', }, });