aboutsummaryrefslogtreecommitdiff
path: root/public/app.js
diff options
context:
space:
mode:
authordax <me@dax.ist>2026-08-04 18:53:03 +0100
committerdax <me@dax.ist>2026-08-04 18:53:03 +0100
commit247390000a0fdf7eeedb89a2c5500c64f36df1c3 (patch)
tree4ffdc7bc608518a2d489375b29d7969768f1a97f /public/app.js
parentdadf0909dd235a7764a59a63d2b51861fa81e385 (diff)
show upload progress and indexing state; raise upload caps and timeouts
Diffstat (limited to 'public/app.js')
-rw-r--r--public/app.js52
1 files changed, 48 insertions, 4 deletions
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")