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 { 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 显示"—") */
export function SpeedPair({ dl, up, className }: { dl: number; up: number; className?: string }) {
return (
{formatSpeed(dl)}
{formatSpeed(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;
}) {
const options: { value: DownloadBoardState; label: string }[] = [
{ value: "active", label: "进行中" },
{ value: "completed", label: "已完成" },
];
return (
{options.map((o) => (
// biome-ignore lint/a11y/useSemanticElements: 分段按钮
))}
);
}
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 (
);
}
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: "下载完成(含做种)后出现在这里。",
},
};