Add resumable downloads, download paths, queue controls and history
This commit is contained in:
1 parent
434f6441da
commit
6027cfabb9
43 files changed
+2138
-291
No files matched your search
+55
-4
@@ -1,13 +1,21 @@
|
||||
import { button, element } from "./dom.js";
|
||||
import { formatBytes, percentOf, statusLabels } from "./format.js";
|
||||
import { formatBytes, formatDuration, percentOf, statusLabels } from "./format.js";
|
||||
|
||||
const activeStatuses = new Set(["queued", "downloading", "extracting", "moving"]);
|
||||
const pausableStatuses = new Set(["queued", "downloading"]);
|
||||
const speedSmoothing = 0.3;
|
||||
const speedSamples = new Map();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function updateSpeeds(jobs) {
|
||||
const now = performance.now();
|
||||
const seenIds = new Set();
|
||||
@@ -17,7 +25,8 @@ export function updateSpeeds(jobs) {
|
||||
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);
|
||||
const instantSpeed = ((job.bytesDone - previous.bytes) * 1000) / (now - previous.time);
|
||||
speed = previous.speed > 0 ? previous.speed + speedSmoothing * (instantSpeed - previous.speed) : instantSpeed;
|
||||
}
|
||||
speedSamples.set(job.id, { bytes: job.bytesDone, time: now, speed });
|
||||
}
|
||||
@@ -27,8 +36,25 @@ export function updateSpeeds(jobs) {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
container.replaceChildren(...jobs.map((job) => renderJob(job, job.id === 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) {
|
||||
@@ -42,6 +68,12 @@ function renderJob(job, selected, handlers) {
|
||||
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";
|
||||
@@ -79,7 +111,12 @@ function progressText(job, percent) {
|
||||
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`);
|
||||
if (isActive(job) && sample && sample.speed > 0) {
|
||||
parts.push(`${formatBytes(sample.speed)}/s`);
|
||||
if (job.bytesTotal > job.bytesDone) {
|
||||
parts.push(`${formatDuration((job.bytesTotal - job.bytesDone) / sample.speed)} left`);
|
||||
}
|
||||
}
|
||||
return parts.join(" | ");
|
||||
}
|
||||
|
||||
@@ -88,6 +125,20 @@ function renderActions(job, handlers) {
|
||||
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)));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user