import React, { useEffect, useState } from 'react'; import { View, Text, SectionList, StyleSheet, TouchableOpacity, Image, Modal, StatusBar, Alert, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useRouter } from 'expo-router'; import { Ionicons } from '@expo/vector-icons'; import Video from 'react-native-video'; let ScreenOrientation: typeof import('expo-screen-orientation') | null = null; try { ScreenOrientation = require('expo-screen-orientation'); } catch {} import { useDownloadStore, DownloadTask } from '../store/downloadStore'; import { LanShareModal } from '../components/LanShareModal'; import { proxyImageUrl } from '../utils/imageUrl'; import { useTheme } from '../utils/theme'; function formatFileSize(bytes?: number): string { if (!bytes || bytes <= 0) return ''; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; } export default function DownloadsScreen() { const router = useRouter(); const theme = useTheme(); const { tasks, loadFromStorage, removeTask } = useDownloadStore(); const [playingUri, setPlayingUri] = useState(null); const [playingTitle, setPlayingTitle] = useState(''); const [shareTask, setShareTask] = useState<(DownloadTask & { key: string }) | null>(null); async function openPlayer(uri: string, title: string) { setPlayingTitle(title); setPlayingUri(uri); await ScreenOrientation?.unlockAsync(); } async function closePlayer() { setPlayingUri(null); await ScreenOrientation?.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP); } function confirmDelete(key: string, status: DownloadTask['status']) { const isDownloading = status === 'downloading'; Alert.alert( isDownloading ? '取消下载' : '删除下载', isDownloading ? '确定取消该下载任务?' : '确定删除该文件?删除后不可恢复。', [ { text: '取消', style: 'cancel' }, { text: isDownloading ? '取消下载' : '删除', style: 'destructive', onPress: () => removeTask(key) }, ], ); } useEffect(() => { loadFromStorage(); }, []); const all = Object.entries(tasks).map(([key, task]) => ({ key, ...task })); const downloading = all.filter((t) => t.status === 'downloading' || t.status === 'error'); const done = all.filter((t) => t.status === 'done'); const sections = []; if (downloading.length > 0) sections.push({ title: '下载中', data: downloading }); if (done.length > 0) sections.push({ title: '已下载', data: done }); return ( router.back()} style={styles.backBtn}> 我的下载 {sections.length === 0 ? ( 暂无下载记录 ) : ( item.key} renderSectionHeader={({ section }) => ( {section.title} )} renderItem={({ item }) => ( { if (item.localUri) openPlayer(item.localUri, item.title); }} onDelete={() => confirmDelete(item.key, item.status)} onShare={() => setShareTask(item)} onRetry={() => router.push(`/video/${item.bvid}` as any)} /> )} ItemSeparatorComponent={() => ( )} contentContainerStyle={{ paddingBottom: 32 }} /> )} setShareTask(null)} /> {/* Local file player modal */} ); } function DownloadRow({ task, theme, onPlay, onDelete, onShare, onRetry, }: { task: DownloadTask & { key: string }; theme: ReturnType; onPlay: () => void; onDelete: () => void; onShare: () => void; onRetry: () => void; }) { const isDone = task.status === 'done'; const isError = task.status === 'error'; const isDownloading = task.status === 'downloading'; const rowContent = ( {task.title} {task.qdesc}{task.fileSize ? ` · ${formatFileSize(task.fileSize)}` : ''} {isDownloading && ( {Math.round(task.progress * 100)}% )} {isError && ( {task.error ?? '下载失败'} 重新下载 )} {isDone && ( )} ); if (isDone) { return ( {rowContent} ); } return rowContent; } const styles = StyleSheet.create({ safe: { flex: 1 }, topBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth, }, backBtn: { padding: 4 }, topTitle: { flex: 1, fontSize: 16, fontWeight: '700', marginLeft: 4, }, empty: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12 }, emptyTxt: { fontSize: 14 }, sectionHeader: { paddingHorizontal: 16, paddingVertical: 8, }, sectionTitle: { fontSize: 13, fontWeight: '600' }, row: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, gap: 12, }, cover: { width: 80, height: 54, borderRadius: 6, backgroundColor: '#eee', flexShrink: 0 }, info: { flex: 1 }, title: { fontSize: 13, lineHeight: 18, marginBottom: 4 }, qdesc: { fontSize: 12, marginBottom: 4 }, progressWrap: { flexDirection: 'row', alignItems: 'center', marginTop: 2, gap: 6 }, progressTrack: { flex: 1, height: 3, borderRadius: 2, backgroundColor: '#e0e0e0', overflow: 'hidden', }, progressFill: { height: 3, backgroundColor: '#00AEEC', borderRadius: 2 }, progressTxt: { fontSize: 11, color: '#999', minWidth: 30 }, errorRow: { flexDirection: 'row', alignItems: 'center', gap: 8, marginTop: 2 }, errorTxt: { fontSize: 12, color: '#f44', flex: 1 }, retryBtn: { paddingHorizontal: 8, paddingVertical: 2, borderRadius: 10, backgroundColor: '#e8f7fd', }, retryTxt: { fontSize: 12, color: '#00AEEC', fontWeight: '600' }, actions: { alignItems: 'center', gap: 12 }, actionBtn: { padding: 4 }, separator: { height: StyleSheet.hairlineWidth }, // player modal playerBg: { flex: 1, backgroundColor: '#000', justifyContent: 'center' }, playerBar: { position: 'absolute', top: 44, left: 0, right: 0, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, backgroundColor: 'rgba(0,0,0,0.4)', paddingVertical: 8, }, closeBtn: { padding: 6 }, playerTitle: { flex: 1, color: '#fff', fontSize: 14, fontWeight: '600', marginLeft: 4 }, });