Files
beStream/src/pages/Downloads.tsx
2025-12-14 12:57:37 +01:00

276 lines
10 KiB
TypeScript

import { motion } from 'framer-motion';
import { Download, Trash2, Pause, Play, FolderOpen, X } from 'lucide-react';
import { Link } from 'react-router-dom';
import Button from '../components/ui/Button';
import ProgressBar from '../components/ui/ProgressBar';
import Badge from '../components/ui/Badge';
import { useDownloadStore } from '../stores/downloadStore';
import { formatBytes, formatSpeed } from '../services/torrent/webtorrent';
export default function Downloads() {
const {
items,
pauseDownload,
resumeDownload,
removeDownload,
clearCompleted,
} = useDownloadStore();
const activeDownloads = items.filter(
(item) => item.status === 'downloading' || item.status === 'queued'
);
const completedDownloads = items.filter((item) => item.status === 'completed');
const failedDownloads = items.filter((item) => item.status === 'error');
const getStatusBadge = (status: string) => {
switch (status) {
case 'downloading':
return <Badge variant="info">Downloading</Badge>;
case 'queued':
return <Badge>Queued</Badge>;
case 'paused':
return <Badge variant="warning">Paused</Badge>;
case 'completed':
return <Badge variant="success">Completed</Badge>;
case 'error':
return <Badge variant="error">Failed</Badge>;
default:
return <Badge>{status}</Badge>;
}
};
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="min-h-screen bg-netflix-black pt-24 pb-16"
>
<div className="max-w-[1920px] mx-auto px-4 md:px-8">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-3xl md:text-4xl font-bold flex items-center gap-3">
<Download className="text-netflix-red" />
Downloads
</h1>
<p className="text-gray-400 mt-1">
{activeDownloads.length} active, {completedDownloads.length} completed
</p>
</div>
{completedDownloads.length > 0 && (
<Button
variant="ghost"
onClick={clearCompleted}
leftIcon={<Trash2 size={18} />}
>
Clear Completed
</Button>
)}
</div>
{/* Active Downloads */}
{activeDownloads.length > 0 && (
<div className="mb-8">
<h2 className="text-xl font-semibold mb-4">Active Downloads</h2>
<div className="space-y-4">
{activeDownloads.map((item, index) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className="p-4 glass rounded-lg"
>
<div className="flex gap-4">
{/* Poster */}
<Link to={`/movie/${item.movie.id}`} className="flex-shrink-0">
<img
src={item.movie.medium_cover_image}
alt={item.movie.title}
className="w-20 md:w-24 rounded-md"
/>
</Link>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between">
<div>
<h3 className="font-semibold line-clamp-1">
{item.movie.title}
</h3>
<div className="flex items-center gap-2 text-sm text-gray-400 mt-1">
<span>{item.torrent.quality}</span>
<span></span>
<span>{item.torrent.size}</span>
</div>
</div>
{getStatusBadge(item.status)}
</div>
{/* Progress */}
<div className="mt-4">
<div className="flex justify-between text-sm mb-1">
<span>{(item.progress * 100).toFixed(1)}%</span>
<span className="text-gray-400">
{formatSpeed(item.downloadSpeed)} {item.peers} peers
</span>
</div>
<ProgressBar progress={item.progress * 100} />
</div>
{/* Actions */}
<div className="flex gap-2 mt-4">
{item.status === 'downloading' ? (
<Button
variant="secondary"
size="sm"
onClick={() => pauseDownload(item.id)}
leftIcon={<Pause size={16} />}
>
Pause
</Button>
) : (
<Button
size="sm"
onClick={() => resumeDownload(item.id)}
leftIcon={<Play size={16} />}
>
Resume
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => {
if (confirm('Cancel this download?')) {
removeDownload(item.id);
}
}}
leftIcon={<X size={16} />}
>
Cancel
</Button>
</div>
</div>
</div>
</motion.div>
))}
</div>
</div>
)}
{/* Completed Downloads */}
{completedDownloads.length > 0 && (
<div className="mb-8">
<h2 className="text-xl font-semibold mb-4">Completed</h2>
<div className="space-y-4">
{completedDownloads.map((item, index) => (
<motion.div
key={item.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.05 }}
className="flex items-center gap-4 p-4 glass rounded-lg"
>
<Link to={`/movie/${item.movie.id}`} className="flex-shrink-0">
<img
src={item.movie.medium_cover_image}
alt={item.movie.title}
className="w-16 rounded-md"
/>
</Link>
<div className="flex-1 min-w-0">
<h3 className="font-semibold line-clamp-1">{item.movie.title}</h3>
<div className="flex items-center gap-2 text-sm text-gray-400">
<span>{item.torrent.quality}</span>
<span></span>
<span>{item.torrent.size}</span>
</div>
</div>
<div className="flex items-center gap-2">
<Link to={`/player/${item.movie.id}`}>
<Button size="sm" leftIcon={<Play size={16} />}>
Play
</Button>
</Link>
{item.filePath && (
<Button
variant="secondary"
size="sm"
leftIcon={<FolderOpen size={16} />}
>
Open
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={() => removeDownload(item.id)}
>
<Trash2 size={16} />
</Button>
</div>
</motion.div>
))}
</div>
</div>
)}
{/* Failed Downloads */}
{failedDownloads.length > 0 && (
<div className="mb-8">
<h2 className="text-xl font-semibold mb-4">Failed</h2>
<div className="space-y-4">
{failedDownloads.map((item) => (
<div
key={item.id}
className="flex items-center gap-4 p-4 glass rounded-lg border border-red-500/20"
>
<img
src={item.movie.medium_cover_image}
alt={item.movie.title}
className="w-16 rounded-md opacity-50"
/>
<div className="flex-1">
<h3 className="font-semibold">{item.movie.title}</h3>
<p className="text-sm text-red-400">{item.error}</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => removeDownload(item.id)}
>
<Trash2 size={16} />
</Button>
</div>
))}
</div>
</div>
)}
{/* Empty State */}
{items.length === 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="text-center py-16"
>
<Download size={64} className="mx-auto text-gray-600 mb-4" />
<h2 className="text-xl text-gray-400 mb-2">No downloads</h2>
<p className="text-gray-500 mb-6">
Download movies for offline viewing
</p>
<Link to="/browse">
<Button>Browse Movies</Button>
</Link>
</motion.div>
)}
</div>
</motion.div>
);
}