From f698ba516d21101e074298077b617bf3a72b6e04 Mon Sep 17 00:00:00 2001 From: Daniel Legt Date: Tue, 2 Jun 2026 22:41:59 +0300 Subject: [PATCH] feat(upload): add transfer rate tracking and 6-hour expiry option - Implement real-time transfer rate tracking and display upload speed (e.g., Mb/s) in the progress status. - Add a 6-hour (360 minutes) option to the upload expiry selection ladder. - Fix an issue where the "new upload" button remained visible by explicitly toggling its display style and adding a CSS fallback for the `hidden` attribute. --- backend/libs/handlers/pages.go | 2 +- backend/static/css/20-upload.css | 4 ++ backend/static/js/40-upload.js | 67 +++++++++++++++++++++++++++----- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/backend/libs/handlers/pages.go b/backend/libs/handlers/pages.go index eaeb2ce..e2cb017 100644 --- a/backend/libs/handlers/pages.go +++ b/backend/libs/handlers/pages.go @@ -95,7 +95,7 @@ func (a *App) homeExpiryOptions(settings services.UploadPolicySettings, user ser } func buildExpiryOptions(maxDays int, unlimited bool) ([]expiryOption, int) { - ladder := []int{60, 720, 1440, 2880, 4320, 7200, 10080, 14400, 20160, 43200, 86400, 129600, 259200, 525600} + ladder := []int{60, 360, 720, 1440, 2880, 4320, 7200, 10080, 14400, 20160, 43200, 86400, 129600, 259200, 525600} capMinutes := maxDays * 24 * 60 if unlimited || capMinutes <= 0 { diff --git a/backend/static/css/20-upload.css b/backend/static/css/20-upload.css index 6c0be8f..7ba6a6a 100644 --- a/backend/static/css/20-upload.css +++ b/backend/static/css/20-upload.css @@ -52,6 +52,10 @@ margin-top: -0.25rem; } +.upload-options .form-footer .upload-new-button[hidden] { + display: none !important; +} + .hero-copy { text-align: center; } diff --git a/backend/static/js/40-upload.js b/backend/static/js/40-upload.js index a376f65..29dc673 100644 --- a/backend/static/js/40-upload.js +++ b/backend/static/js/40-upload.js @@ -117,9 +117,7 @@ uploadQueue.hidden = true; uploadQueue.replaceChildren(); } - if (newUpload) { - newUpload.hidden = true; - } + updateNewUploadVisibility(); if (fileSummary) { fileSummary.textContent = "Upload complete."; } @@ -189,9 +187,16 @@ uploadQueue.hidden = true; uploadQueue.replaceChildren(); } - if (newUpload) { - newUpload.hidden = !(resumeMode && recoveredDraft); + updateNewUploadVisibility(); + } + + function updateNewUploadVisibility() { + if (!newUpload) { + return; } + const visible = Boolean(resumeMode && recoveredDraft); + newUpload.hidden = !visible; + newUpload.style.display = visible ? "" : "none"; } function setLoading(isLoading, submit) { @@ -216,6 +221,41 @@ } } + function updateUploadProgress(percent, bytesPerSecond) { + const clamped = Math.max(0, Math.min(100, Math.round(percent || 0))); + const rate = formatTransferRate(bytesPerSecond); + updateStatus(rate ? `${clamped}% · ${rate}` : `${clamped}%`); + } + + function createTransferRateTracker(initialBytes) { + const startedAt = performance.now(); + const baseline = Math.max(0, initialBytes || 0); + let lastRate = 0; + return function track(currentBytes) { + const elapsedSeconds = (performance.now() - startedAt) / 1000; + const transferred = Math.max(0, (currentBytes || 0) - baseline); + if (elapsedSeconds < 0.25 || transferred <= 0) { + return lastRate; + } + lastRate = transferred / elapsedSeconds; + return lastRate; + }; + } + + function formatTransferRate(bytesPerSecond) { + if (!Number.isFinite(bytesPerSecond) || bytesPerSecond <= 0) { + return ""; + } + const units = ["b/s", "Kb/s", "Mb/s", "Gb/s"]; + let value = bytesPerSecond * 8; + let unit = 0; + while (value >= 1000 && unit < units.length - 1) { + value /= 1000; + unit += 1; + } + return `${value >= 10 || unit === 0 ? value.toFixed(0) : value.toFixed(1)} ${units[unit]}`; + } + function renderResult(payload) { if (!result || !resultList || !resultMeta || !openBox) { return; @@ -248,16 +288,18 @@ function uploadWithProgress(url, formData, files) { return new Promise((resolve, reject) => { const request = new XMLHttpRequest(); + const rateTracker = createTransferRateTracker(0); request.open("POST", url); request.setRequestHeader("Accept", "application/json"); request.upload.addEventListener("progress", (event) => { + const rate = rateTracker(event.loaded || 0); if (!event.lengthComputable) { - updateStatus("Uploading..."); + updateStatus(rate > 0 ? `Uploading · ${formatTransferRate(rate)}` : "Uploading..."); return; } const percent = Math.round((event.loaded / event.total) * 100); - updateStatus(`${percent}%`); + updateUploadProgress(percent, rate); setTotalProgress(percent); setFileProgress(files, percent); }); @@ -348,7 +390,9 @@ completedByFile[index] = uploadedBytesForSessionFile(sessionFile, session.chunkSize); setSingleFileProgress(index, files[index], percentForBytes(completedByFile[index], files[index].size)); }); - setTotalProgress(percentForBytes(completedByFile.reduce((sum, bytes) => sum + bytes, 0), totalBytes)); + const initiallyUploadedBytes = completedByFile.reduce((sum, bytes) => sum + bytes, 0); + const rateTracker = createTransferRateTracker(initiallyUploadedBytes); + setTotalProgress(percentForBytes(initiallyUploadedBytes, totalBytes)); for (let fileIndex = 0; fileIndex < files.length; fileIndex++) { const file = files[fileIndex]; @@ -362,9 +406,11 @@ const end = Math.min(file.size, start + session.chunkSize); await uploadChunkWithRetry(session, sessionFile, chunkIndex, file.slice(start, end), (loaded) => { const currentTotal = completedByFile.reduce((sum, bytes) => sum + bytes, 0) + loaded; - setTotalProgress(percentForBytes(currentTotal, totalBytes)); + const percent = percentForBytes(currentTotal, totalBytes); + const rate = rateTracker(currentTotal); + setTotalProgress(percent); setSingleFileProgress(fileIndex, file, percentForBytes(completedByFile[fileIndex] + loaded, file.size)); - updateStatus(`${percentForBytes(currentTotal, totalBytes)}%`); + updateUploadProgress(percent, rate); }); completedByFile[fileIndex] += end - start; uploaded.add(chunkIndex); @@ -749,6 +795,7 @@ selectedFiles = []; renderResumeQueue(recoveredDraft.session, selectedFiles); updateSelectedState(); + updateNewUploadVisibility(); updateStatus("Drop or reselect missing files to continue. Extra files will be added to this upload."); }