mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
267 lines
7.0 KiB
JavaScript
267 lines
7.0 KiB
JavaScript
const COLUMN_DEFS = [
|
|
{
|
|
key: "name",
|
|
label: "Name",
|
|
headerClass: "w-full sm:w-1/3 text-left",
|
|
cellClass: "font-medium text-text truncate",
|
|
},
|
|
{
|
|
key: "progress",
|
|
label: "Progress",
|
|
headerClass: "w-24 text-right",
|
|
cellClass: "tabular-nums text-right",
|
|
formatter: "progress",
|
|
},
|
|
{
|
|
key: "downloadSpeed",
|
|
label: "\u2193 Speed",
|
|
headerClass: "w-24 text-right",
|
|
cellClass: "tabular-nums text-right",
|
|
formatter: "bytesPerSecond",
|
|
},
|
|
{
|
|
key: "uploadSpeed",
|
|
label: "\u2191 Speed",
|
|
headerClass: "w-24 text-right",
|
|
cellClass: "tabular-nums text-right",
|
|
formatter: "bytesPerSecond",
|
|
},
|
|
{
|
|
key: "numPeers",
|
|
label: "Peers",
|
|
headerClass: "w-20 text-right",
|
|
cellClass: "tabular-nums text-right",
|
|
formatter: "integer",
|
|
},
|
|
{
|
|
key: "ratio",
|
|
label: "Ratio",
|
|
headerClass: "w-20 text-right",
|
|
cellClass: "tabular-nums text-right",
|
|
formatter: "ratio",
|
|
},
|
|
];
|
|
|
|
function getRowId(torrent) {
|
|
if (!torrent) {
|
|
return "";
|
|
}
|
|
|
|
return torrent.infoHash || torrent.magnetURI || torrent.name || String(Math.random());
|
|
}
|
|
|
|
function isSelectedTorrent(selected, torrent) {
|
|
if (!selected || !torrent) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
selected === torrent ||
|
|
(selected.magnetURI && selected.magnetURI === torrent.magnetURI) ||
|
|
(selected.infoHash && selected.infoHash === torrent.infoHash)
|
|
);
|
|
}
|
|
|
|
function formatProgress(value) {
|
|
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
return "";
|
|
}
|
|
|
|
return `${(value * 100).toFixed(1)}%`;
|
|
}
|
|
|
|
function formatRatio(value) {
|
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
return "0.00";
|
|
}
|
|
|
|
return value.toFixed(2);
|
|
}
|
|
|
|
function formatInteger(value) {
|
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
return "0";
|
|
}
|
|
|
|
return Math.max(0, Math.floor(value)).toString();
|
|
}
|
|
|
|
function formatBytesPerSecond(value, formatter) {
|
|
if (typeof formatter === "function") {
|
|
return formatter(value, true) || "";
|
|
}
|
|
|
|
if (typeof value !== "number" || Number.isNaN(value) || value < 1) {
|
|
return "";
|
|
}
|
|
|
|
const units = ["B", "kB", "MB", "GB", "TB"];
|
|
const exponent = Math.min(Math.floor(Math.log(value) / Math.log(1000)), units.length - 1);
|
|
const val = (value / Math.pow(1000, exponent)).toFixed(1);
|
|
return `${val} ${units[exponent]}/s`;
|
|
}
|
|
|
|
function resolveFormatter(column, customFormatters) {
|
|
const name = column.formatter;
|
|
if (typeof name === "function") {
|
|
return name;
|
|
}
|
|
|
|
const userFormatter = customFormatters?.[name];
|
|
if (typeof userFormatter === "function") {
|
|
return userFormatter;
|
|
}
|
|
|
|
switch (name) {
|
|
case "progress":
|
|
return formatProgress;
|
|
case "ratio":
|
|
return formatRatio;
|
|
case "integer":
|
|
return formatInteger;
|
|
case "bytesPerSecond":
|
|
return (value) => formatBytesPerSecond(value, customFormatters?.bytes);
|
|
default:
|
|
return (value) => value;
|
|
}
|
|
}
|
|
|
|
export function createTorrentTable({
|
|
documentRef = typeof document !== "undefined" ? document : null,
|
|
root,
|
|
onSelect,
|
|
formatters = {},
|
|
} = {}) {
|
|
if (!documentRef) {
|
|
throw new Error("createTorrentTable requires a document reference");
|
|
}
|
|
|
|
if (!(root instanceof documentRef.defaultView.HTMLElement)) {
|
|
throw new Error("createTorrentTable requires a valid root element");
|
|
}
|
|
|
|
const doc = documentRef;
|
|
root.innerHTML = "";
|
|
|
|
const container = doc.createElement("div");
|
|
container.className = "overflow-hidden rounded-xl border border-border-translucent bg-surface shadow-sm";
|
|
|
|
const scroll = doc.createElement("div");
|
|
scroll.className = "overflow-x-auto";
|
|
container.appendChild(scroll);
|
|
|
|
const table = doc.createElement("table");
|
|
table.className = "min-w-full divide-y divide-border-translucent text-sm";
|
|
scroll.appendChild(table);
|
|
|
|
const thead = doc.createElement("thead");
|
|
thead.className = "bg-surface-alt text-muted-strong";
|
|
const headerRow = doc.createElement("tr");
|
|
COLUMN_DEFS.forEach((column) => {
|
|
const th = doc.createElement("th");
|
|
th.setAttribute("scope", "col");
|
|
th.className = "px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide";
|
|
if (column.headerClass) {
|
|
th.className += ` ${column.headerClass}`;
|
|
}
|
|
th.textContent = column.label;
|
|
headerRow.appendChild(th);
|
|
});
|
|
thead.appendChild(headerRow);
|
|
table.appendChild(thead);
|
|
|
|
const tbody = doc.createElement("tbody");
|
|
tbody.className = "divide-y divide-border-translucent";
|
|
table.appendChild(tbody);
|
|
|
|
const emptyBody = doc.createElement("tbody");
|
|
const emptyRow = doc.createElement("tr");
|
|
const emptyCell = doc.createElement("td");
|
|
emptyCell.className = "px-4 py-8 text-center text-sm text-muted";
|
|
emptyCell.colSpan = COLUMN_DEFS.length;
|
|
emptyCell.textContent = "No active torrents yet. Add a magnet link to get started.";
|
|
emptyRow.appendChild(emptyCell);
|
|
emptyBody.appendChild(emptyRow);
|
|
table.appendChild(emptyBody);
|
|
|
|
root.appendChild(container);
|
|
|
|
function render(torrents = [], selectedTorrent = null) {
|
|
const rows = Array.isArray(torrents) ? torrents : [];
|
|
tbody.innerHTML = "";
|
|
|
|
if (!rows.length) {
|
|
tbody.hidden = true;
|
|
emptyBody.hidden = false;
|
|
return;
|
|
}
|
|
|
|
tbody.hidden = false;
|
|
emptyBody.hidden = true;
|
|
|
|
for (const torrent of rows) {
|
|
const row = doc.createElement("tr");
|
|
row.className = [
|
|
"group",
|
|
"align-middle",
|
|
"cursor-pointer",
|
|
"transition",
|
|
"hover:bg-panel-hover",
|
|
"focus-visible:outline",
|
|
"focus-visible:outline-2",
|
|
"focus-visible:outline-offset-2",
|
|
"focus-visible:outline-info",
|
|
].join(" ");
|
|
row.setAttribute("role", "button");
|
|
row.setAttribute("tabindex", "0");
|
|
row.dataset.torrentId = getRowId(torrent);
|
|
|
|
if (isSelectedTorrent(selectedTorrent, torrent)) {
|
|
row.classList.add("bg-info/10", "text-text");
|
|
row.setAttribute("aria-pressed", "true");
|
|
} else {
|
|
row.setAttribute("aria-pressed", "false");
|
|
}
|
|
|
|
row.addEventListener("click", () => {
|
|
if (typeof onSelect === "function") {
|
|
onSelect(torrent);
|
|
}
|
|
});
|
|
|
|
row.addEventListener("keydown", (event) => {
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
if (typeof onSelect === "function") {
|
|
onSelect(torrent);
|
|
}
|
|
}
|
|
});
|
|
|
|
COLUMN_DEFS.forEach((column) => {
|
|
const td = doc.createElement("td");
|
|
td.className = "px-4 py-3";
|
|
if (column.cellClass) {
|
|
td.className += ` ${column.cellClass}`;
|
|
}
|
|
|
|
const value = torrent ? torrent[column.key] : undefined;
|
|
const formatter = resolveFormatter(column, formatters);
|
|
const displayValue = formatter(value, torrent, column);
|
|
const span = doc.createElement("span");
|
|
span.textContent = displayValue == null ? "" : String(displayValue);
|
|
td.appendChild(span);
|
|
row.appendChild(td);
|
|
});
|
|
|
|
tbody.appendChild(row);
|
|
}
|
|
}
|
|
|
|
function destroy() {
|
|
root.innerHTML = "";
|
|
}
|
|
|
|
return { render, destroy };
|
|
}
|