import type { DownloadBoard, DownloadBoardItem, DownloadBoardPlanItem, DownloadBoardRow, DownloadBoardState, } from "@umt/contracts"; import { ArrowDown, ArrowUp, CheckCircle2, Download } from "lucide-react"; import type { ReactNode } from "react"; import { Link } from "react-router"; import type { DataColumn } from "~/components/console/data-table.tsx"; import { consolePath } from "~/components/console/nav.ts"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { SegmentedControl } from "~/components/ui/segmented.tsx"; import { relTime } from "~/components/workbench/release-fields.ts"; import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts"; import { formatEta, formatSpeed, liveStateLabel, organizeState, peersText, progressOf } from "./board-format.ts"; import { DownloadStatusBadges, PauseResume, ProgressLine } from "./download-detail.tsx"; import { PlanActions, PlanWaitBadge } from "./plan-row.tsx"; // 下载面板(DL-2):顶部总速度 + 「进行中 / 已完成」分段;进行中看进度与速度,已完成看完成时间、分享率与整理状态。 // DL-3:进行中里还有没提交到下载器的下载(等你批准 / 等磁盘空间…),与下载任务按加入时间一起排。 /** 下载 / 上传速度一对(行内 0 显示"—";总速度一行传 showZero,空闲时显示 0 B/s,不留两道横线) */ export function SpeedPair({ dl, up, className, showZero = false, }: { dl: number; up: number; className?: string; showZero?: boolean; }) { const fmt = (v: number) => (showZero && !(v > 0) ? "0 B/s" : formatSpeed(v)); return ( {fmt(dl)} {fmt(up)} ); } /** 总速度一行;不可达的下载器单独列出 */ export function TransferLine({ transfer }: { transfer: DownloadBoard["transfer"] | undefined }) { if (!transfer || transfer.instances.length === 0) return null; const down = transfer.instances.filter((i) => !i.reachable); const perInstance = transfer.instances.length > 1 ? transfer.instances.map((i) => `${i.name}:↓ ${formatSpeed(i.dlSpeed)} ↑ ${formatSpeed(i.upSpeed)}`).join("\n") : undefined; return (
总速度 {down.length ? ( {down.map((i) => `「${i.name}」`).join("")}连接失败 ) : null}
); } export function BoardSegments({ value, counts, onChange, }: { value: DownloadBoardState; counts: DownloadBoard["counts"] | undefined; onChange: (v: DownloadBoardState) => void; }) { return ( ); } function WorkLine({ t, extra }: { t: Pick; extra?: ReactNode }) { if (!t.media && !extra) return null; return (

{t.media ? ( e.stopPropagation()} className="truncate underline-offset-2 hover:text-foreground hover:underline" > {t.media.title} {t.media.year ? ` (${t.media.year})` : ""} ) : null} {extra}

); } function NameCell({ t, children }: { t: Pick; children?: ReactNode }) { return (

{t.name}

{children}
); } function ActiveStatus({ t }: { t: DownloadBoardItem }) { const live = liveStateLabel(t.live?.rawState); return ( {live ? {live} : null} ); } const dash = —; /** 还没提交的下载:名称列(窄屏带状态与操作) */ function PlanNameCell({ p }: { p: DownloadBoardPlanItem }) { return ( {formatBytes(p.sizeBytes, 1)} : null} />
); } export function activeColumns(): DataColumn[] { return [ { key: "name", header: "下载任务", cell: (t) => t.kind === "plan" ? ( ) : ( {/* 窄屏:进度与速度并进名称列 */}
{t.live?.dlSpeed || t.live?.upSpeed ? (

{t.live.etaSec !== null ? 剩余 {formatEta(t.live.etaSec)} : null}

) : null}
), }, { key: "progress", header: "进度", hideBelow: "md", className: "w-40", cell: (t) => (t.kind === "plan" ? dash : ), }, { key: "speed", header: "速度", hideBelow: "md", cell: (t) => t.kind === "plan" || (!t.live?.dlSpeed && !t.live?.upSpeed) ? ( dash ) : (

{formatSpeed(t.live?.dlSpeed)}

{formatSpeed(t.live?.upSpeed)}

), }, { key: "eta", header: "剩余时间", hideBelow: "lg", cell: (t) => t.kind === "plan" ? ( dash ) : (

{formatEta(t.live?.etaSec)}

{peersText(t.live) ?

{peersText(t.live)}

: null}
), }, { key: "status", header: "状态", hideBelow: "md", cell: (t) => (t.kind === "plan" ? : ), }, { key: "ops", header: "", align: "right", cell: (t) => t.kind === "plan" ? ( ) : ( ), }, ]; } function OrganizeBadge({ t }: { t: DownloadBoardItem }) { const s = organizeState(t); if (!s) return —; const badge = {s.label}; return (
{s.to ? ( e.stopPropagation()} title={t.organize?.decision ? "去整理记录处理" : "查看整理记录"} className="rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring" > {badge} ) : ( badge )} {s.hint ? {s.hint} : null}
); } /** 完成时间以下载器记录为准(接管的任务在数据库里记的是接管时刻) */ const completedAtOf = (t: DownloadBoardItem) => t.live?.completedAt ?? t.completedAt ?? null; const sizeText = (t: DownloadBoardItem) => (t.sizeBytes === null ? "未知" : formatBytes(t.sizeBytes, 1)); const ratioOf = (t: DownloadBoardItem) => t.live?.ratio ?? t.ratio; export function completedColumns(): DataColumn[] { return [ { key: "name", header: "下载任务", cell: (t) => (

{sizeText(t)} {relTime(completedAtOf(t))}完成 {ratioOf(t) !== null ? 分享率 {ratioOf(t)?.toFixed(2)} : null} {t.live?.upSpeed ? ( {formatSpeed(t.live.upSpeed)} ) : null}

), }, { key: "size", header: "大小", hideBelow: "md", align: "right", cell: (t) => {sizeText(t)}, }, { key: "done", header: "完成时间", hideBelow: "md", cell: (t) => ( {relTime(completedAtOf(t))} ), }, { key: "ratio", header: "分享率 / 上传", hideBelow: "lg", cell: (t) => (

{ratioOf(t) === null ? "—" : ratioOf(t)?.toFixed(2)}

{t.live?.upSpeed ? (

{formatSpeed(t.live.upSpeed)}

) : null}
), }, { key: "organize", header: "整理", cell: (t) => }, ]; } export const BOARD_EMPTY: Record = { active: { icon: Download, title: "没有进行中的下载", description: "自动收录和手动下载的任务都在这里,包括等批准、等磁盘空间的。下载完成后移到「已完成」。", }, completed: { icon: CheckCircle2, title: "还没有下载完成的任务", description: "下载完成的任务在这里,包括做种中的。", }, };