From 247390000a0fdf7eeedb89a2c5500c64f36df1c3 Mon Sep 17 00:00:00 2001 From: dax Date: Tue, 4 Aug 2026 18:53:03 +0100 Subject: show upload progress and indexing state; raise upload caps and timeouts --- public/app.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 4 deletions(-) (limited to 'public/app.js') diff --git a/public/app.js b/public/app.js index dfd6e6b..78584bf 100644 --- a/public/app.js +++ b/public/app.js @@ -8,6 +8,9 @@ const importEl = $("#import") const fileInput = $("#file") const dropOverlay = $("#drop") const toastEl = $("#toast") +const progressEl = $("#progress") +const progressFill = $("#progress-fill") +const progressText = $("#progress-text") addEventListener("error", (e) => { toast(`error: ${e.message || "script failed"}`, true) @@ -708,23 +711,64 @@ function plural(n) { return n === 1 ? "photo" : "photos" } +function setProgress(mode, pct, text) { + progressEl.hidden = false + progressText.textContent = text + if (mode === "indeterminate") { + progressEl.classList.add("indeterminate") + } else { + progressEl.classList.remove("indeterminate") + progressFill.style.width = pct + "%" + } +} + +function hideProgress() { + progressEl.hidden = true + progressEl.classList.remove("indeterminate") + progressFill.style.width = "0%" +} + +function uploadWithProgress(form, onPct, onUploaded) { + return new Promise((resolve, reject) => { + const xhr = new XMLHttpRequest() + xhr.open("POST", "/api/import") + xhr.upload.addEventListener("progress", (e) => { + if (e.lengthComputable) onPct(Math.min(100, Math.round((e.loaded / e.total) * 100))) + }) + xhr.upload.addEventListener("load", () => onUploaded()) + xhr.addEventListener("load", () => { + let data = {} + try { data = JSON.parse(xhr.responseText) } catch { /* ignore */ } + if (xhr.status >= 200 && xhr.status < 300) resolve(data) + else reject(new Error(data.error || `import failed (${xhr.status})`)) + }) + xhr.addEventListener("error", () => reject(new Error("upload failed (network error)"))) + xhr.send(form) + }) +} + async function importFiles(list) { const files = Array.from(list || []).filter((f) => f.size > 0) if (!files.length) return const form = new FormData() for (const f of files) form.append("photos", f, f.name) importEl.classList.add("busy") - toast(`importing ${files.length} ${plural(files.length)}...`) + setProgress("determinate", 0, `uploading ${files.length} ${plural(files.length)}… 0%`) try { - const r = await fetch("/api/import", { method: "POST", body: form }) - const data = await r.json().catch(() => ({})) - if (!r.ok || !data.ok) throw new Error(data.error || `import failed (${r.status})`) + const data = await uploadWithProgress( + form, + (pct) => setProgress("determinate", pct, `uploading ${files.length} ${plural(files.length)}… ${pct}%`), + () => setProgress("indeterminate", 100, "indexing…") + ) + if (!data.ok) throw new Error(data.error || "import failed") exitsCache.clear() exitPending.clear() + hideProgress() hideImportState() await beginWalk() toast(`imported ${data.imported} ${plural(data.imported)} - keep walking to find them`) } catch (err) { + hideProgress() toast(err.message || "import failed", true) } finally { importEl.classList.remove("busy") -- cgit v1.3.1