import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { View, Text, StyleSheet, StatusBar, Dimensions, TouchableOpacity, FlatList, RefreshControl, Alert, Platform, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useNavigation, useFocusEffect } from '@react-navigation/native'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import * as Haptics from 'expo-haptics'; import { useTheme } from '../contexts/ThemeContext'; import Animated, { useAnimatedStyle, useSharedValue, withTiming, withSpring, } from 'react-native-reanimated'; import { NavigationProp } from '@react-navigation/native'; import { RootStackParamList } from '../navigation/AppNavigator'; import { LinearGradient } from 'expo-linear-gradient'; import { useDownloads } from '../contexts/DownloadsContext'; import type { DownloadItem } from '../contexts/DownloadsContext'; const { height, width } = Dimensions.get('window'); // Download items come from DownloadsContext // Empty state component const EmptyDownloadsState: React.FC = () => { const { currentTheme } = useTheme(); return ( No Downloads Yet Downloaded content will appear here for offline viewing { // Navigate to search or home to find content }} > Explore Content ); }; // Download item component const DownloadItemComponent: React.FC<{ item: DownloadItem; onPress: (item: DownloadItem) => void; onAction: (item: DownloadItem, action: 'pause' | 'resume' | 'cancel' | 'retry') => void; }> = React.memo(({ item, onPress, onAction }) => { const { currentTheme } = useTheme(); const formatBytes = (bytes?: number) => { if (!bytes || bytes <= 0) return '0 B'; const sizes = ['B','KB','MB','GB','TB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); const v = bytes / Math.pow(1024, i); return `${v.toFixed(v >= 100 ? 0 : v >= 10 ? 1 : 2)} ${sizes[i]}`; }; const getStatusColor = () => { switch (item.status) { case 'downloading': return currentTheme.colors.primary; case 'completed': return currentTheme.colors.success; case 'paused': return currentTheme.colors.warning; case 'error': return currentTheme.colors.error; case 'queued': return currentTheme.colors.mediumEmphasis; default: return currentTheme.colors.mediumEmphasis; } }; const getStatusText = () => { switch (item.status) { case 'downloading': const eta = item.etaSeconds ? `${Math.ceil(item.etaSeconds / 60)}m` : undefined; return eta ? `Downloading • ${eta}` : 'Downloading'; case 'completed': return 'Completed'; case 'paused': return 'Paused'; case 'error': return 'Error'; case 'queued': return 'Queued'; default: return 'Unknown'; } }; const getActionIcon = () => { switch (item.status) { case 'downloading': // return 'pause'; // Resume support commented out return null; case 'paused': case 'error': // return 'play'; // Resume support commented out return null; case 'queued': // return 'play'; // Resume support commented out return null; default: return null; } }; const handleActionPress = () => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); switch (item.status) { case 'downloading': // onAction(item, 'pause'); // Resume support commented out break; case 'paused': case 'error': case 'queued': // onAction(item, 'resume'); // Resume support commented out break; } }; return ( onPress(item)} activeOpacity={0.8} > {/* Content info */} {item.title}{item.type === 'series' && item.season && item.episode ? ` S${String(item.season).padStart(2,'0')}E${String(item.episode).padStart(2,'0')}` : ''} {item.type === 'series' && ( S{item.season?.toString().padStart(2, '0')}E{item.episode?.toString().padStart(2, '0')} • {item.episodeTitle} )} {/* Progress section */} {/* Provider + quality row */} {(item.providerName || 'Provider') + (item.quality ? ` ${item.quality}` : '')} {/* Status row */} {getStatusText()} {/* Size row */} {formatBytes(item.downloadedBytes)} / {item.totalBytes ? formatBytes(item.totalBytes) : '—'} {/* Progress bar */} {item.progress || 0}% {item.etaSeconds && item.status === 'downloading' && ( {Math.ceil(item.etaSeconds / 60)}m remaining )} {/* Action buttons */} {getActionIcon() && ( )} { Alert.alert( 'Remove Download', 'Are you sure you want to remove this download?', [ { text: 'Cancel', style: 'cancel' }, { text: 'Remove', style: 'destructive', onPress: () => onAction(item, 'cancel') }, ] ); }} activeOpacity={0.7} > ); }); const DownloadsScreen: React.FC = () => { const navigation = useNavigation>(); const { currentTheme } = useTheme(); const { top: safeAreaTop } = useSafeAreaInsets(); const { downloads, /*pauseDownload, resumeDownload,*/ cancelDownload } = useDownloads(); const [isRefreshing, setIsRefreshing] = useState(false); const [selectedFilter, setSelectedFilter] = useState<'all' | 'downloading' | 'completed'>('all'); // Animation values const headerOpacity = useSharedValue(1); // Filter downloads based on selected filter const filteredDownloads = useMemo(() => { if (selectedFilter === 'all') return downloads; return downloads.filter(item => { switch (selectedFilter) { case 'downloading': return item.status === 'downloading' || item.status === 'queued'; case 'completed': return item.status === 'completed'; // case 'paused': // return item.status === 'paused' || item.status === 'error'; // Resume support commented out default: return true; } }); }, [downloads, selectedFilter]); // Statistics const stats = useMemo(() => { const total = downloads.length; const downloading = downloads.filter(item => item.status === 'downloading' || item.status === 'queued' ).length; const completed = downloads.filter(item => item.status === 'completed').length; // const paused = downloads.filter(item => // item.status === 'paused' || item.status === 'error' // ).length; // Resume support commented out return { total, downloading, completed /*, paused*/ }; }, [downloads]); // Handlers const handleRefresh = useCallback(async () => { setIsRefreshing(true); // In a real app, this would refresh the downloads from the service await new Promise(resolve => setTimeout(resolve, 1000)); setIsRefreshing(false); }, []); const handleDownloadPress = useCallback((item: DownloadItem) => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); if (item.status !== 'completed') { Alert.alert('Download not ready', 'Please wait until the download completes.'); return; } const uri = (item as any).fileUri || (item as any).sourceUrl; if (!uri) return; // Infer videoType and mkv const lower = String(uri).toLowerCase(); const isMkv = /\.mkv(\?|$)/i.test(lower) || /(?:[?&]ext=|container=|format=)mkv\b/i.test(lower); const isM3u8 = /\.m3u8(\?|$)/i.test(lower); const isMpd = /\.mpd(\?|$)/i.test(lower); const isMp4 = /\.mp4(\?|$)/i.test(lower); const videoType = isM3u8 ? 'm3u8' : isMpd ? 'mpd' : isMp4 ? 'mp4' : undefined; const playerRoute = Platform.OS === 'ios' ? 'PlayerIOS' : 'PlayerAndroid'; navigation.navigate(playerRoute as any, { uri, title: item.title, episodeTitle: item.type === 'series' ? item.episodeTitle : undefined, season: item.type === 'series' ? item.season : undefined, episode: item.type === 'series' ? item.episode : undefined, quality: item.quality, year: undefined, streamProvider: 'Downloads', streamName: item.providerName || 'Offline', headers: undefined, forceVlc: Platform.OS === 'android' ? isMkv : false, id: item.id, type: item.type, episodeId: undefined, imdbId: undefined, availableStreams: {}, backdrop: undefined, videoType, } as any); }, [navigation]); const handleDownloadAction = useCallback((item: DownloadItem, action: 'pause' | 'resume' | 'cancel' | 'retry') => { // if (action === 'pause') pauseDownload(item.id); // if (action === 'resume') resumeDownload(item.id); if (action === 'cancel') cancelDownload(item.id); }, [/*pauseDownload, resumeDownload,*/ cancelDownload]); const handleFilterPress = useCallback((filter: 'all' | 'downloading' | 'completed') => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); setSelectedFilter(filter); }, []); // Focus effect useFocusEffect( useCallback(() => { // In a real app, this would load downloads from the service // For now, we'll just show empty state }, []) ); // Animated styles const headerStyle = useAnimatedStyle(() => ({ opacity: headerOpacity.value, })); const renderFilterButton = (filter: typeof selectedFilter, label: string, count: number) => ( handleFilterPress(filter)} activeOpacity={0.8} > {label} {count > 0 && ( {count} )} ); return ( {/* Header */} Downloads {downloads.length > 0 && ( {renderFilterButton('all', 'All', stats.total)} {renderFilterButton('downloading', 'Active', stats.downloading)} {renderFilterButton('completed', 'Done', stats.completed)} {/* {renderFilterButton('paused', 'Paused', stats.paused)} */} {/* Resume support commented out */} )} {/* Content */} {downloads.length === 0 ? ( ) : ( item.id} renderItem={({ item }) => ( )} contentContainerStyle={styles.listContainer} showsVerticalScrollIndicator={false} refreshControl={ } ListEmptyComponent={() => ( No {selectedFilter} downloads Try selecting a different filter )} /> )} ); }; const styles = StyleSheet.create({ container: { flex: 1, }, header: { paddingHorizontal: 20, paddingBottom: 16, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: 'rgba(255, 255, 255, 0.1)', }, headerTitle: { fontSize: 32, fontWeight: '700', marginBottom: 16, }, filterContainer: { flexDirection: 'row', gap: 12, }, filterButton: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 8, borderRadius: 20, gap: 8, }, filterButtonText: { fontSize: 14, fontWeight: '600', }, filterBadge: { minWidth: 20, height: 20, borderRadius: 10, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 6, }, filterBadgeText: { fontSize: 12, fontWeight: '700', }, listContainer: { padding: 20, paddingTop: 8, }, downloadItem: { borderRadius: 16, padding: 16, marginBottom: 12, flexDirection: 'row', alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 8, elevation: 3, }, downloadContent: { flex: 1, marginRight: 12, }, downloadHeader: { marginBottom: 12, }, titleContainer: { flexDirection: 'row', alignItems: 'center', marginBottom: 4, gap: 8, }, downloadTitle: { fontSize: 16, fontWeight: '600', flex: 1, }, qualityBadge: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 6, }, qualityText: { fontSize: 12, fontWeight: '700', }, episodeInfo: { fontSize: 14, fontWeight: '500', }, progressSection: { gap: 4, }, providerRow: { marginBottom: 2, }, providerText: { fontSize: 12, fontWeight: '500', }, statusRow: { marginBottom: 2, }, sizeRow: { marginBottom: 6, }, progressInfo: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 8, }, statusText: { fontSize: 13, fontWeight: '600', }, progressText: { fontSize: 12, fontWeight: '500', }, progressContainer: { height: 4, borderRadius: 2, overflow: 'hidden', }, progressBar: { height: '100%', borderRadius: 2, }, progressDetails: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', }, progressPercentage: { fontSize: 14, fontWeight: '700', }, etaText: { fontSize: 12, fontWeight: '500', }, actionContainer: { flexDirection: 'row', gap: 8, }, actionButton: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', }, emptyContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 40, }, emptyIconContainer: { width: 96, height: 96, borderRadius: 48, alignItems: 'center', justifyContent: 'center', marginBottom: 24, }, emptyTitle: { fontSize: 24, fontWeight: '700', marginBottom: 8, textAlign: 'center', }, emptySubtitle: { fontSize: 16, textAlign: 'center', lineHeight: 24, marginBottom: 32, }, exploreButton: { paddingHorizontal: 24, paddingVertical: 12, borderRadius: 24, }, exploreButtonText: { fontSize: 16, fontWeight: '600', }, emptyFilterContainer: { alignItems: 'center', justifyContent: 'center', paddingVertical: 60, }, emptyFilterTitle: { fontSize: 18, fontWeight: '600', marginTop: 16, marginBottom: 8, }, emptyFilterSubtitle: { fontSize: 14, textAlign: 'center', }, }); export default DownloadsScreen;