import { button, element } from "./dom.js"; import { formatBytes, percentOf, statusLabels } from "./format.js"; const activeStatuses = new Set(["queued", "downloading", "extracting", "moving"]); const speedSamples = new Map(); export function isActive(job) { return activeStatuses.has(job.status); } export function updateSpeeds(jobs) { const now = performance.now(); const seenIds = new Set(); for (const job of jobs) { seenIds.add(job.id); const previous = speedSamples.get(job.id); let speed = 0; if (previous && job.bytesDone >= previous.bytes && now > previous.time) { speed = ((job.bytesDone - previous.bytes) * 1000) / (now - previous.time); } speedSamples.set(job.id, { bytes: job.bytesDone, time: now, speed }); } for (const id of speedSamples.keys()) { if (!seenIds.has(id)) speedSamples.delete(id); } } export function renderJobs(container, jobs, selectedId, handlers) { container.replaceChildren(...jobs.map((job) => renderJob(job, job.id === selectedId, handlers))); } 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.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 sample = speedSamples.get(job.id); if (isActive(job) && sample && sample.speed > 0) parts.push(`${formatBytes(sample.speed)}/s`); 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.status !== "moving") { actions.append(button(isActive(job) ? "Cancel" : "Remove", "danger small", () => handlers.onDelete(job))); } return actions; }