Files

126 lines
4.5 KiB
JavaScript
Executable File

import { button, element } from "./dom.js";
import { formatBytes, formatRemaining, percentOf, statusLabels } from "./format.js";
import { speedOf } from "./speed.js";
const activeStatuses = new Set(["queued", "downloading", "extracting", "moving"]);
const pausableStatuses = new Set(["queued", "downloading"]);
export function isActive(job) {
return activeStatuses.has(job.status);
}
export function matchesFilter(job, filter) {
if (filter === "all") return true;
if (filter === "active") return isActive(job);
return job.status === filter;
}
function displayRank(job) {
if (isActive(job) && !job.queuePosition) return 0;
if (job.queuePosition) return 1;
return 2;
}
function sortForDisplay(jobs) {
return [...jobs].sort((first, second) => {
const rankDifference = displayRank(first) - displayRank(second);
if (rankDifference !== 0) return rankDifference;
if (first.queuePosition && second.queuePosition) return first.queuePosition - second.queuePosition;
return 0;
});
}
export function renderJobs(container, jobs, selectedId, handlers) {
const queueLength = jobs.filter((job) => job.queuePosition).length;
const context = { ...handlers, queueLength };
container.replaceChildren(...sortForDisplay(jobs).map((job) => renderJob(job, job.id === selectedId, context)));
}
function renderJob(job, selected, handlers) {
const item = element("li", selected ? "job selected" : "job");
const header = element("div", "job-header");
header.append(
element("span", "job-name", job.fileName || job.sourceUrl),
element("span", `badge badge-${job.status}`, statusLabels[job.status] || job.status),
);
item.append(header);
if (job.sourceUrl) item.append(element("p", "job-source muted", job.sourceUrl));
if (job.moveTarget && job.status !== "completed") {
item.append(element("p", "job-source muted", `Download to: ${job.moveTarget}`));
}
if (job.queuePosition) {
item.append(element("p", "job-source muted", `Queue position ${job.queuePosition} of ${handlers.queueLength}`));
}
if (job.status !== "completed") item.append(renderProgress(job));
if (job.message) {
const messageClass = job.status === "failed" ? "job-message error" : "job-message muted";
item.append(element("p", messageClass, job.message));
}
item.append(renderActions(job, handlers));
return item;
}
function renderProgress(job) {
const wrapper = element("div");
const track = element("div", "progress");
const bar = element("div", "progress-bar");
const percent = percentOf(job.bytesDone, job.bytesTotal);
if (percent === null && isActive(job)) {
bar.classList.add("indeterminate");
} else {
const width = percent === null ? (job.status === "ready" ? 100 : 0) : percent;
bar.style.width = `${width}%`;
}
track.append(bar);
wrapper.append(track, element("div", "progress-text muted", progressText(job, percent)));
return wrapper;
}
function progressText(job, percent) {
const parts = [];
if (job.bytesTotal > 0) {
parts.push(`${formatBytes(job.bytesDone)} of ${formatBytes(job.bytesTotal)}`);
} else if (job.bytesDone > 0) {
parts.push(formatBytes(job.bytesDone));
}
if (percent !== null) parts.push(`${percent.toFixed(1)}%`);
const speed = speedOf(job.id);
if (isActive(job) && speed > 0) {
parts.push(`${formatBytes(speed)}/s`);
if (job.bytesTotal > job.bytesDone) {
parts.push(`${formatRemaining((job.bytesTotal - job.bytesDone) / speed)} left`);
}
}
return parts.join(" | ");
}
function renderActions(job, handlers) {
const actions = element("div", "job-actions");
if (job.status === "ready") {
actions.append(button("Browse files", "secondary small", () => handlers.onBrowse(job)));
}
if (job.queuePosition > 1) {
actions.append(button("Start next", "secondary small", () => handlers.onReorder(job, "first")));
actions.append(button("Move up", "secondary small", () => handlers.onReorder(job, "up")));
}
if (job.queuePosition && job.queuePosition < handlers.queueLength) {
actions.append(button("Move down", "secondary small", () => handlers.onReorder(job, "down")));
}
if (pausableStatuses.has(job.status)) {
actions.append(button("Pause", "secondary small", () => handlers.onPause(job)));
}
if (job.status === "paused" || job.status === "failed") {
const label = job.status === "paused" ? "Resume" : "Retry";
actions.append(button(label, "secondary small", () => handlers.onResume(job)));
}
if (job.status !== "moving") {
actions.append(button(isActive(job) ? "Cancel" : "Remove", "danger small", () => handlers.onDelete(job)));
}
return actions;
}