import React, { useCallback, useMemo, memo, useState, useEffect } from 'react'; import { View, Text, StyleSheet, TouchableOpacity, ActivityIndicator, FlatList, SectionList, Platform, ImageBackground, ScrollView, StatusBar, Alert } from 'react-native'; import { useRoute, useNavigation } from '@react-navigation/native'; import { RouteProp } from '@react-navigation/native'; import { NavigationProp } from '@react-navigation/native'; import { MaterialIcons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; import { Image } from 'expo-image'; import { RootStackParamList, RootStackNavigationProp } from '../navigation/AppNavigator'; import { useMetadata } from '../hooks/useMetadata'; import { colors } from '../styles/colors'; import { Stream } from '../types/metadata'; import { tmdbService } from '../services/tmdbService'; import { stremioService } from '../services/stremioService'; import { VideoPlayerService } from '../services/videoPlayerService'; import { useSettings } from '../hooks/useSettings'; import Animated, { FadeIn, FadeInDown, SlideInDown, withSpring, withTiming, useAnimatedStyle, useSharedValue, interpolate, Extrapolate, runOnJS, cancelAnimation, SharedValue } from 'react-native-reanimated'; import { torrentService } from '../services/torrentService'; import { TorrentProgress } from '../services/torrentService'; const TMDB_LOGO = 'https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/Tmdb.new.logo.svg/512px-Tmdb.new.logo.svg.png?20200406190906'; const HDR_ICON = 'https://uxwing.com/wp-content/themes/uxwing/download/video-photography-multimedia/hdr-icon.png'; const DOLBY_ICON = 'https://upload.wikimedia.org/wikipedia/en/thumb/3/3f/Dolby_Vision_%28logo%29.svg/512px-Dolby_Vision_%28logo%29.svg.png?20220908042900'; // Extracted Components const StreamCard = memo(({ stream, onPress, index, torrentProgress, isLoading, statusMessage }: { stream: Stream; onPress: () => void; index: number; torrentProgress?: TorrentProgress; isLoading?: boolean; statusMessage?: string; }) => { const quality = stream.title?.match(/(\d+)p/)?.[1] || null; const isHDR = stream.title?.toLowerCase().includes('hdr'); const isDolby = stream.title?.toLowerCase().includes('dolby') || stream.title?.includes('DV'); const size = stream.title?.match(/💾\s*([\d.]+\s*[GM]B)/)?.[1]; const isTorrent = stream.url?.startsWith('magnet:') || stream.behaviorHints?.isMagnetStream; const isDebrid = stream.behaviorHints?.cached; const displayTitle = stream.name || stream.title || 'Unnamed Stream'; const displayAddonName = stream.title || ''; const entering = useMemo(() => FadeInDown .delay(50 + Math.min(index, 10) * 30) .springify() .damping(15) .mass(0.9) , [index]); const handlePress = useCallback(() => { console.log('StreamCard pressed:', { isTorrent, isDebrid, hasProgress: !!torrentProgress, url: stream.url, behaviorHints: stream.behaviorHints }); onPress(); }, [isTorrent, isDebrid, torrentProgress, stream.url, stream.behaviorHints, onPress]); // Only disable if it's a torrent that's not debrid and not currently downloading const isDisabled = isTorrent && !isDebrid && !torrentProgress && !stream.behaviorHints?.notWebReady; // Keep track of downloading status const isDownloading = !!torrentProgress && isTorrent; return ( {displayTitle} {displayAddonName && displayAddonName !== displayTitle && ( {displayAddonName} )} {/* Show loading indicator if stream is loading */} {isLoading && ( {statusMessage || "Loading..."} )} {/* Show download indicator for active downloads */} {isDownloading && ( Downloading... )} {quality && ( {quality}p )} {isHDR && ( HDR )} {isDolby && ( DOLBY )} {size && ( {size} )} {isTorrent && !isDebrid && ( TORRENT )} {isDebrid && ( DEBRID )} {/* Render progress bar if there's progress */} {torrentProgress && ( {`${Math.round(torrentProgress.bufferProgress)}% • ${Math.round(torrentProgress.downloadSpeed / 1024)} KB/s • ${torrentProgress.seeds} seeds`} )} ); }, (prevProps, nextProps) => { // Custom comparison to prevent unnecessary re-renders return prevProps.stream.url === nextProps.stream.url && prevProps.index === nextProps.index && prevProps.torrentProgress?.bufferProgress === nextProps.torrentProgress?.bufferProgress; }); const QualityTag = React.memo(({ text, color }: { text: string; color: string }) => ( {text} )); const ProviderFilter = memo(({ selectedProvider, providers, onSelect }: { selectedProvider: string; providers: Array<{ id: string; name: string; }>; onSelect: (id: string) => void; }) => { const renderItem = useCallback(({ item }: { item: { id: string; name: string } }) => ( onSelect(item.id)} > {item.name} ), [selectedProvider, onSelect]); return ( item.id} horizontal showsHorizontalScrollIndicator={false} style={styles.filterScroll} bounces={true} overScrollMode="never" decelerationRate="fast" initialNumToRender={5} maxToRenderPerBatch={3} windowSize={3} getItemLayout={(data, index) => ({ length: 100, // Approximate width of each item offset: 100 * index, index, })} /> ); }); export const StreamsScreen = () => { const route = useRoute>(); const navigation = useNavigation(); const { id, type, episodeId } = route.params; const { settings } = useSettings(); // Add timing logs const [loadStartTime, setLoadStartTime] = useState(0); const [providerLoadTimes, setProviderLoadTimes] = useState<{[key: string]: number}>({}); const { metadata, episodes, groupedStreams, loadingStreams, episodeStreams, loadingEpisodeStreams, selectedEpisode, loadStreams, loadEpisodeStreams, setSelectedEpisode, groupedEpisodes, } = useMetadata({ id, type }); const [selectedProvider, setSelectedProvider] = React.useState('all'); const [availableProviders, setAvailableProviders] = React.useState>(new Set()); // Optimize animation values with cleanup const headerOpacity = useSharedValue(0); const heroScale = useSharedValue(0.95); const filterOpacity = useSharedValue(0); // Add new state for torrent progress const [torrentProgress, setTorrentProgress] = React.useState<{[key: string]: TorrentProgress}>({}); const [activeTorrent, setActiveTorrent] = React.useState(null); // Add new state to track video player status const [isVideoPlaying, setIsVideoPlaying] = React.useState(false); // Add state for provider loading status const [loadingProviders, setLoadingProviders] = useState<{[key: string]: boolean}>({}); // Add state for more detailed provider loading tracking const [providerStatus, setProviderStatus] = useState<{ [key: string]: { loading: boolean; success: boolean; error: boolean; message: string; timeStarted: number; timeCompleted: number; } }>({}); // Monitor streams loading start useEffect(() => { if (loadingStreams || loadingEpisodeStreams) { console.log("⏱️ Stream loading started"); const now = Date.now(); setLoadStartTime(now); setProviderLoadTimes({}); // Reset provider status setProviderStatus({ 'source_1': { loading: true, success: false, error: false, message: 'Loading...', timeStarted: now, timeCompleted: 0 }, 'source_2': { loading: true, success: false, error: false, message: 'Loading...', timeStarted: now, timeCompleted: 0 }, 'stremio': { loading: true, success: false, error: false, message: 'Loading...', timeStarted: now, timeCompleted: 0 } }); // Also update the simpler loading state setLoadingProviders({ 'source_1': true, 'source_2': true, 'stremio': true }); } }, [loadingStreams, loadingEpisodeStreams]); // Monitor new provider results as they appear useEffect(() => { const streams = type === 'series' ? episodeStreams : groupedStreams; const now = Date.now(); // Check for new providers Object.keys(streams).forEach(provider => { // Identify the parent provider (source_1, source_2, stremio addon) let parentProvider = provider; if (provider !== 'source_1' && provider !== 'source_2') { parentProvider = 'stremio'; } // Update provider status when new streams appear setProviderStatus(prev => { const loadTime = now - loadStartTime; console.log(`✅ Provider "${parentProvider}" loaded successfully after ${loadTime}ms with ${streams[provider].streams.length} streams`); // Only update if it was previously loading if (prev[parentProvider]?.loading) { return { ...prev, [parentProvider]: { ...prev[parentProvider], loading: false, success: true, message: `Loaded ${streams[provider].streams.length} streams`, timeCompleted: now } }; } return prev; }); // Update the simpler loading state setLoadingProviders((prev: {[key: string]: boolean}) => ({...prev, [parentProvider]: false})); }); }, [episodeStreams, groupedStreams, type, loadStartTime]); // Mark loading as complete when all loading is done useEffect(() => { if (!loadingStreams && !loadingEpisodeStreams) { // Check for any providers that are still marked as loading but didn't complete setProviderStatus(prev => { const updatedStatus = {...prev}; let updated = false; Object.keys(updatedStatus).forEach(provider => { if (updatedStatus[provider]?.loading) { updatedStatus[provider] = { ...updatedStatus[provider], loading: false, error: true, message: 'Failed to load', timeCompleted: Date.now() }; updated = true; console.log(`⚠️ Provider "${provider}" timed out or failed`); // Update the simpler loading state setLoadingProviders((prevLoading: {[key: string]: boolean}) => ({...prevLoading, [provider]: false})); } }); return updated ? updatedStatus : prev; }); } }, [loadingStreams, loadingEpisodeStreams]); React.useEffect(() => { if (type === 'series' && episodeId) { console.log(`🎬 Loading episode streams for: ${episodeId}`); setLoadingProviders({ 'source_1': true, 'source_2': true, 'stremio': true }); setSelectedEpisode(episodeId); loadEpisodeStreams(episodeId); } else if (type === 'movie') { console.log(`🎬 Loading movie streams for: ${id}`); setLoadingProviders({ 'source_1': true, 'source_2': true, 'stremio': true }); loadStreams(); } }, [type, episodeId]); React.useEffect(() => { const streams = type === 'series' ? episodeStreams : groupedStreams; const providers = new Set(Object.keys(streams)); setAvailableProviders(providers); }, [type, groupedStreams, episodeStreams]); React.useEffect(() => { // Trigger entrance animations headerOpacity.value = withTiming(1, { duration: 400 }); heroScale.value = withSpring(1, { damping: 15, stiffness: 100, mass: 0.9, restDisplacementThreshold: 0.01 }); filterOpacity.value = withTiming(1, { duration: 500 }); return () => { // Cleanup animations on unmount cancelAnimation(headerOpacity); cancelAnimation(heroScale); cancelAnimation(filterOpacity); }; }, []); // Memoize handlers const handleBack = useCallback(() => { const cleanup = () => { headerOpacity.value = withTiming(0, { duration: 200 }); heroScale.value = withTiming(0.95, { duration: 200 }); filterOpacity.value = withTiming(0, { duration: 200 }); }; cleanup(); // For series episodes, always replace current screen with metadata screen if (type === 'series') { navigation.replace('Metadata', { id: id, type: type }); } else { navigation.goBack(); } }, [navigation, headerOpacity, heroScale, filterOpacity, type, id]); const handleProviderChange = useCallback((provider: string) => { setSelectedProvider(provider); }, []); const currentEpisode = useMemo(() => { if (!selectedEpisode) return null; // Search through all episodes in all seasons const allEpisodes = Object.values(groupedEpisodes).flat(); return allEpisodes.find(ep => ep.stremioId === selectedEpisode || `${id}:${ep.season_number}:${ep.episode_number}` === selectedEpisode ); }, [selectedEpisode, groupedEpisodes, id]); // Update handleStreamPress const handleStreamPress = useCallback(async (stream: Stream) => { try { if (stream.url) { console.log('handleStreamPress called with stream:', { url: stream.url, behaviorHints: stream.behaviorHints, isMagnet: stream.url.startsWith('magnet:'), isMagnetStream: stream.behaviorHints?.isMagnetStream, useExternalPlayer: settings.useExternalPlayer }); // Check if it's a magnet link either directly or through behaviorHints const isMagnet = stream.url.startsWith('magnet:') || stream.behaviorHints?.isMagnetStream; if (isMagnet) { console.log('Handling magnet link...'); // Check if there's already an active torrent if (activeTorrent && activeTorrent !== stream.url) { Alert.alert( 'Active Download', 'There is already an active download. Do you want to stop it and start this one?', [ { text: 'Cancel', style: 'cancel' }, { text: 'Stop and Switch', style: 'destructive', onPress: async () => { console.log('Stopping current torrent and starting new one'); await torrentService.stopStreamAndWait(); setActiveTorrent(null); setTorrentProgress({}); startTorrentStream(stream); } } ] ); return; } console.log('Starting torrent stream...'); startTorrentStream(stream); } else { console.log('Playing regular stream...'); // Check if external player is enabled in settings if (settings.useExternalPlayer) { console.log('Using external player for URL:', stream.url); // Use VideoPlayerService to launch external player try { const videoPlayerService = VideoPlayerService; await videoPlayerService.playVideo(stream.url, { useExternalPlayer: true, title: metadata?.name || '', episodeTitle: type === 'series' ? currentEpisode?.name : undefined, episodeNumber: type === 'series' ? `S${currentEpisode?.season_number}E${currentEpisode?.episode_number}` : undefined, releaseDate: metadata?.year?.toString(), }); } catch (externalPlayerError) { console.error('External player error:', externalPlayerError); // Fallback to built-in player if external player fails navigation.navigate('Player', { uri: stream.url, title: metadata?.name || '', episodeTitle: type === 'series' ? currentEpisode?.name : undefined, season: type === 'series' ? currentEpisode?.season_number : undefined, episode: type === 'series' ? currentEpisode?.episode_number : undefined, quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, year: metadata?.year, streamProvider: stream.name }); } } else { // Use built-in player navigation.navigate('Player', { uri: stream.url, title: metadata?.name || '', episodeTitle: type === 'series' ? currentEpisode?.name : undefined, season: type === 'series' ? currentEpisode?.season_number : undefined, episode: type === 'series' ? currentEpisode?.episode_number : undefined, quality: stream.title?.match(/(\d+)p/)?.[1] || undefined, year: metadata?.year, streamProvider: stream.name }); } } } } catch (error) { console.error('Stream error:', error); Alert.alert( 'Playback Error', error instanceof Error ? error.message : 'An error occurred while playing the video' ); } }, [metadata, type, currentEpisode, activeTorrent, navigation, settings.useExternalPlayer]); // Clean up torrent when component unmounts or when returning from player React.useEffect(() => { const unsubscribe = navigation.addListener('focus', () => { // This runs when returning from the player screen console.log('[StreamsScreen] Screen focused, checking if cleanup needed'); if (isVideoPlaying) { console.log('[StreamsScreen] Playback ended, cleaning up torrent'); setIsVideoPlaying(false); // Clean up the torrent when returning from video player if (activeTorrent) { console.log('[StreamsScreen] Stopping torrent after playback'); torrentService.stopStreamAndWait().catch(error => { console.error('[StreamsScreen] Error during cleanup:', error); }); setActiveTorrent(null); setTorrentProgress({}); } } }); return () => { unsubscribe(); console.log('[StreamsScreen] Component unmounting, cleaning up torrent'); if (activeTorrent) { console.log('[StreamsScreen] Stopping torrent on unmount'); torrentService.stopStreamAndWait().catch(error => { console.error('[StreamsScreen] Error during cleanup:', error); }); } }; }, [navigation, activeTorrent, isVideoPlaying]); const startTorrentStream = useCallback(async (stream: Stream) => { if (!stream.url) return; try { console.log('[StreamsScreen] Starting torrent stream with URL:', stream.url); // Make sure any existing stream is fully stopped if (activeTorrent && activeTorrent !== stream.url) { await torrentService.stopStreamAndWait(); setActiveTorrent(null); setTorrentProgress({}); } setActiveTorrent(stream.url); setIsVideoPlaying(false); const videoPath = await torrentService.startStream(stream.url, { onProgress: (progress) => { // Check if progress object is valid and has data if (!progress || Object.keys(progress).length === 0) { console.log('[StreamsScreen] Received empty progress object, ignoring'); return; } console.log('[StreamsScreen] Torrent progress update:', { url: stream.url, progress, currentTorrentProgress: torrentProgress[stream.url!] }); // Validate progress values before updating state if (typeof progress.bufferProgress === 'number' || typeof progress.downloadSpeed === 'number' || typeof progress.progress === 'number' || typeof progress.seeds === 'number') { setTorrentProgress(prev => ({ ...prev, [stream.url!]: progress })); } } }); console.log('[StreamsScreen] Got video path:', videoPath); // Once we have the video file path, play it using VideoPlayer screen if (videoPath) { setIsVideoPlaying(true); try { if (settings.useExternalPlayer) { console.log('[StreamsScreen] Using external player for torrent video path:', videoPath); // Use VideoPlayerService to launch external player try { const videoPlayerService = VideoPlayerService; await videoPlayerService.playVideo(`file://${videoPath}`, { useExternalPlayer: true, title: metadata?.name || '', episodeTitle: type === 'series' ? currentEpisode?.name : undefined, episodeNumber: type === 'series' ? `S${currentEpisode?.season_number}E${currentEpisode?.episode_number}` : undefined, releaseDate: metadata?.year?.toString(), }); } catch (externalPlayerError) { console.error('[StreamsScreen] External player error:', externalPlayerError); // Fallback to built-in player if external player fails navigation.navigate('Player', { uri: `file://${videoPath}`, title: metadata?.name || '', episodeTitle: type === 'series' ? currentEpisode?.name : undefined, season: type === 'series' ? currentEpisode?.season_number : undefined, episode: type === 'series' ? currentEpisode?.episode_number : undefined, year: metadata?.year }); } } else { // Use built-in player navigation.navigate('Player', { uri: `file://${videoPath}`, title: metadata?.name || '', episodeTitle: type === 'series' ? currentEpisode?.name : undefined, season: type === 'series' ? currentEpisode?.season_number : undefined, episode: type === 'series' ? currentEpisode?.episode_number : undefined, year: metadata?.year }); } // Note: Cleanup happens in the focus effect when returning from the player } catch (playerError) { console.error('[StreamsScreen] Video player navigation error:', playerError); setIsVideoPlaying(false); // Also stop the torrent on player error console.log('[StreamsScreen] Stopping torrent after player error'); await torrentService.stopStreamAndWait(); setActiveTorrent(null); setTorrentProgress({}); throw playerError; } } else { // If we didn't get a video path, there's a problem console.error('[StreamsScreen] No video path returned from torrent service'); Alert.alert( 'Playback Error', 'No video file found in torrent' ); await torrentService.stopStreamAndWait(); setActiveTorrent(null); setTorrentProgress({}); } } catch (error) { console.error('[StreamsScreen] Torrent error:', error); // Clean up on error setIsVideoPlaying(false); await torrentService.stopStreamAndWait(); setActiveTorrent(null); setTorrentProgress({}); Alert.alert( 'Download Error', error instanceof Error ? error.message : 'An error occurred while playing the video' ); } }, [metadata, type, currentEpisode, torrentProgress, activeTorrent, navigation, settings.useExternalPlayer]); const filterItems = useMemo(() => { const installedAddons = stremioService.getInstalledAddons(); const streams = type === 'series' ? episodeStreams : groupedStreams; return [ { id: 'all', name: 'All Providers' }, ...Array.from(availableProviders) .sort((a, b) => { const indexA = installedAddons.findIndex(addon => addon.id === a); const indexB = installedAddons.findIndex(addon => addon.id === b); if (indexA !== -1 && indexB !== -1) return indexA - indexB; if (indexA !== -1) return -1; if (indexB !== -1) return 1; return 0; }) .map(provider => { const addonInfo = streams[provider]; const installedAddon = installedAddons.find(addon => addon.id === provider); let displayName = provider; if (provider === 'source_1') displayName = 'Source 1'; else if (provider === 'source_2') displayName = 'Source 2'; else if (provider === 'external_sources') displayName = 'External Sources'; else if (installedAddon) displayName = installedAddon.name; else if (addonInfo?.addonName) displayName = addonInfo.addonName; return { id: provider, name: displayName }; }) ]; }, [availableProviders, type, episodeStreams, groupedStreams]); const sections = useMemo(() => { const streams = type === 'series' ? episodeStreams : groupedStreams; const installedAddons = stremioService.getInstalledAddons(); // Remove test addon section return Object.entries(streams) .filter(([addonId]) => { // Filter out test_addon and source_1 if (addonId === 'test_addon' || addonId === 'source_1') return false; return selectedProvider === 'all' || selectedProvider === addonId; }) .sort(([addonIdA], [addonIdB]) => { const indexA = installedAddons.findIndex(addon => addon.id === addonIdA); const indexB = installedAddons.findIndex(addon => addon.id === addonIdB); if (indexA !== -1 && indexB !== -1) return indexA - indexB; if (indexA !== -1) return -1; if (indexB !== -1) return 1; return 0; }) .map(([addonId, { addonName, streams }]) => ({ title: addonName, addonId, data: streams })); }, [selectedProvider, type, episodeStreams, groupedStreams]); const episodeImage = useMemo(() => { if (!currentEpisode) return null; if (currentEpisode.still_path) { return tmdbService.getImageUrl(currentEpisode.still_path, 'original'); } return metadata?.poster || null; }, [currentEpisode, metadata]); const isLoading = type === 'series' ? loadingEpisodeStreams : loadingStreams; const streams = type === 'series' ? episodeStreams : groupedStreams; const heroStyle = useAnimatedStyle(() => ({ transform: [{ scale: heroScale.value }], opacity: headerOpacity.value })); const filterStyle = useAnimatedStyle(() => ({ opacity: filterOpacity.value, transform: [ { translateY: interpolate( filterOpacity.value, [0, 1], [20, 0], Extrapolate.CLAMP ) } ] })); const renderItem = useCallback(({ item, index, section }: { item: Stream; index: number; section: any }) => { const stream = item; const progress = torrentProgress[stream.url!]; const isLoading = loadingProviders[section.addonId]; return ( handleStreamPress(stream)} index={index} torrentProgress={progress} isLoading={isLoading} statusMessage={providerStatus[section.addonId]?.message} /> ); }, [handleStreamPress, torrentProgress, loadingProviders, providerStatus]); const renderSectionHeader = useCallback(({ section }: { section: { title: string } }) => ( {section.title} ), []); return ( {type === 'series' ? 'Back to Episodes' : 'Back to Info'} {type === 'series' && currentEpisode && ( {currentEpisode.episodeString} {currentEpisode.name} {currentEpisode.overview && ( {currentEpisode.overview} )} {tmdbService.formatAirDate(currentEpisode.air_date)} {currentEpisode.vote_average > 0 && ( {currentEpisode.vote_average.toFixed(1)} )} )} {Object.keys(streams).length > 0 && ( )} {isLoading && Object.keys(streams).length === 0 ? ( Finding available streams... ) : Object.keys(streams).length === 0 ? ( No streams available ) : ( `${item.url}-${index}`} renderItem={renderItem} renderSectionHeader={renderSectionHeader} stickySectionHeadersEnabled={false} initialNumToRender={8} maxToRenderPerBatch={4} windowSize={5} removeClippedSubviews={true} contentContainerStyle={styles.streamsContainer} style={styles.streamsContent} showsVerticalScrollIndicator={false} bounces={true} overScrollMode="never" getItemLayout={(data, index) => ({ length: 86, // Height of each stream card + margin offset: 86 * index, index, })} maintainVisibleContentPosition={{ minIndexForVisible: 0, autoscrollToTopThreshold: 10 }} ListFooterComponent={ isLoading ? ( Loading more sources... ) : null } /> )} ); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: colors.darkBackground, }, backButtonContainer: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 1, }, backButton: { flexDirection: 'row', alignItems: 'center', gap: 8, padding: 14, paddingTop: Platform.OS === 'android' ? 35 : 45, }, backButtonText: { color: colors.highEmphasis, fontSize: 13, fontWeight: '600', }, streamsMainContent: { flex: 1, backgroundColor: colors.darkBackground, paddingTop: 20, }, streamsMainContentMovie: { paddingTop: Platform.OS === 'android' ? 90 : 100, }, filterContainer: { paddingHorizontal: 16, paddingBottom: 12, }, filterScroll: { flexGrow: 0, }, filterChip: { backgroundColor: colors.transparentLight, paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, marginRight: 8, borderWidth: 1, borderColor: colors.transparent, }, filterChipSelected: { backgroundColor: colors.transparentLight, borderColor: colors.primary, }, filterChipText: { color: colors.text, fontWeight: '500', }, filterChipTextSelected: { color: colors.primary, fontWeight: 'bold', }, streamsContent: { flex: 1, width: '100%', }, streamsContainer: { paddingHorizontal: 16, paddingBottom: 16, width: '100%', }, streamGroup: { marginBottom: 24, width: '100%', }, streamGroupTitle: { color: colors.text, fontSize: 16, fontWeight: '600', marginBottom: 4, marginTop: 0, backgroundColor: 'transparent', }, streamCard: { flexDirection: 'row', alignItems: 'flex-start', padding: 12, borderRadius: 12, marginBottom: 8, minHeight: 70, backgroundColor: colors.elevation1, borderWidth: 1, borderColor: 'rgba(255,255,255,0.05)', width: '100%', }, streamCardDisabled: { backgroundColor: colors.elevation2, }, streamCardLoading: { opacity: 0.7, }, streamDetails: { flex: 1, }, streamNameRow: { flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', width: '100%', flexWrap: 'wrap', gap: 8 }, streamTitleContainer: { flex: 1, }, streamName: { fontSize: 14, fontWeight: '600', marginBottom: 2, lineHeight: 20, color: colors.highEmphasis, }, streamAddonName: { fontSize: 13, lineHeight: 18, color: colors.mediumEmphasis, marginBottom: 6, }, streamMetaRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 4, marginBottom: 6, alignItems: 'center', }, chip: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 4, marginRight: 4, marginBottom: 4, }, chipText: { color: colors.highEmphasis, fontSize: 12, fontWeight: '600', }, progressContainer: { height: 20, backgroundColor: colors.transparentLight, borderRadius: 10, overflow: 'hidden', marginBottom: 6, }, progressBar: { height: '100%', backgroundColor: colors.primary, }, progressText: { color: colors.highEmphasis, fontSize: 12, fontWeight: '600', marginLeft: 8, }, streamAction: { width: 40, height: 40, borderRadius: 20, backgroundColor: colors.elevation2, justifyContent: 'center', alignItems: 'center', }, skeletonCard: { opacity: 0.7, }, skeletonTitle: { height: 24, width: '40%', backgroundColor: colors.transparentLight, borderRadius: 4, marginBottom: 16, }, skeletonIcon: { width: 24, height: 24, borderRadius: 12, backgroundColor: colors.transparentLight, marginRight: 12, }, skeletonText: { height: 16, borderRadius: 4, marginBottom: 8, backgroundColor: colors.transparentLight, }, skeletonTag: { width: 60, height: 20, borderRadius: 4, marginRight: 8, backgroundColor: colors.transparentLight, }, noStreams: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 32, }, noStreamsText: { color: colors.textMuted, fontSize: 16, marginTop: 16, }, streamsHeroContainer: { width: '100%', height: 300, marginBottom: 0, position: 'relative', backgroundColor: colors.black, }, streamsHeroBackground: { width: '100%', height: '100%', backgroundColor: colors.black, }, streamsHeroGradient: { flex: 1, justifyContent: 'flex-end', padding: 16, paddingBottom: 0, }, streamsHeroContent: { width: '100%', }, streamsHeroInfo: { width: '100%', }, streamsHeroEpisodeNumber: { color: colors.primary, fontSize: 14, fontWeight: 'bold', marginBottom: 2, textShadowColor: 'rgba(0,0,0,0.75)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 2, }, streamsHeroTitle: { color: colors.highEmphasis, fontSize: 24, fontWeight: 'bold', marginBottom: 4, textShadowColor: 'rgba(0,0,0,0.75)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 3, }, streamsHeroOverview: { color: colors.mediumEmphasis, fontSize: 14, lineHeight: 20, marginBottom: 2, textShadowColor: 'rgba(0,0,0,0.75)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 2, }, streamsHeroMeta: { flexDirection: 'row', alignItems: 'center', gap: 12, marginTop: 0, }, streamsHeroReleased: { color: colors.mediumEmphasis, fontSize: 14, textShadowColor: 'rgba(0,0,0,0.75)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 2, }, streamsHeroRating: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(0,0,0,0.7)', paddingHorizontal: 6, paddingVertical: 3, borderRadius: 4, marginTop: 0, }, tmdbLogo: { width: 20, height: 14, }, streamsHeroRatingText: { color: '#01b4e4', fontSize: 13, fontWeight: '700', marginLeft: 4, }, loadingContainer: { alignItems: 'center', paddingVertical: 24, }, loadingText: { color: colors.primary, fontSize: 12, marginLeft: 4, fontWeight: '500', }, downloadingIndicator: { flexDirection: 'row', alignItems: 'center', backgroundColor: colors.transparentLight, paddingHorizontal: 8, paddingVertical: 2, borderRadius: 12, marginLeft: 8, }, downloadingText: { color: colors.primary, fontSize: 12, marginLeft: 4, fontWeight: '500', }, loadingIndicator: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, paddingVertical: 2, borderRadius: 12, marginLeft: 8, }, footerLoading: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', padding: 16, }, footerLoadingText: { color: colors.primary, fontSize: 12, marginLeft: 8, fontWeight: '500', }, }); export default memo(StreamsScreen);