import React, { useEffect, useState } from 'react'; import { View, Text, SectionList, StyleSheet, TouchableOpacity, Image, ActivityIndicator, Modal, StatusBar, useWindowDimensions, } 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'; 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`; } import { proxyImageUrl } from '../utils/imageUrl'; import { useTheme } from '../utils/theme'; 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); const { width, height } = useWindowDimensions(); const isLandscape = width > height; 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); } 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={() => removeTask(item.key)} onShare={() => setShareTask(item)} /> )} ItemSeparatorComponent={() => } contentContainerStyle={{ paddingBottom: 32 }} /> )} setShareTask(null)} /> {/* Local file player modal */} ); } function DownloadRow({ task, onPlay, onDelete, onShare, }: { task: DownloadTask & { key: string }; onPlay: () => void; onDelete: () => void; onShare: () => void; }) { return ( {task.title} {task.qdesc}{task.fileSize ? ` · ${formatFileSize(task.fileSize)}` : ''} {task.status === 'downloading' && ( {Math.round(task.progress * 100)}% )} {task.status === 'error' && ( {task.error ?? '下载失败'} )} {task.status === 'done' && ( <> 播放 )} ); } const styles = StyleSheet.create({ safe: { flex: 1, backgroundColor: '#fff' }, topBar: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: '#eee', }, backBtn: { padding: 4 }, topTitle: { flex: 1, fontSize: 16, fontWeight: '700', color: '#212121', marginLeft: 4, }, empty: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 12 }, emptyTxt: { fontSize: 14, color: '#bbb' }, sectionHeader: { backgroundColor: '#f4f4f4', paddingHorizontal: 16, paddingVertical: 8, }, sectionTitle: { fontSize: 13, fontWeight: '600', color: '#555' }, row: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, backgroundColor: '#fff', gap: 12, }, cover: { width: 80, height: 54, borderRadius: 6, backgroundColor: '#eee' }, info: { flex: 1 }, title: { fontSize: 13, color: '#212121', lineHeight: 18, marginBottom: 4 }, qdesc: { fontSize: 12, color: '#999', marginBottom: 4 }, progressWrap: { flexDirection: 'row', alignItems: 'center', marginTop: 2 }, progressTrack: { flex: 1, height: 3, borderRadius: 2, backgroundColor: '#e0e0e0', overflow: 'hidden', }, progressFill: { height: 3, backgroundColor: '#00AEEC', borderRadius: 2 }, progressTxt: { fontSize: 11, color: '#999', marginLeft: 4 }, errorTxt: { fontSize: 12, color: '#f44', marginTop: 2 }, actions: { alignItems: 'center', gap: 8 }, playBtn: { flexDirection: 'row', alignItems: 'center', gap: 3 }, playTxt: { fontSize: 13, color: '#00AEEC' }, shareBtn: { padding: 4 }, deleteBtn: { padding: 4 }, separator: { height: StyleSheet.hairlineWidth, backgroundColor: '#f0f0f0', marginLeft: 108 }, // player modal playerBg: { flex: 1, backgroundColor: '#000', justifyContent: 'center' }, playerBar: { position: 'absolute', top: 44, left: 0, right: 0, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 8, }, closeBtn: { padding: 6 }, playerTitle: { flex: 1, color: '#fff', fontSize: 14, fontWeight: '600', marginLeft: 4 }, });