aboutsummaryrefslogtreecommitdiff
path: root/public/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'public/app.js')
-rw-r--r--public/app.js161
1 files changed, 122 insertions, 39 deletions
diff --git a/public/app.js b/public/app.js
index 11f19b9..dfd6e6b 100644
--- a/public/app.js
+++ b/public/app.js
@@ -3,8 +3,15 @@
const $ = (s) => document.querySelector(s)
const stage = $("#stage")
const world = $("#world")
-const splash = $("#splash")
-const startBtn = $("#start")
+const importState = $("#import-state")
+const importEl = $("#import")
+const fileInput = $("#file")
+const dropOverlay = $("#drop")
+const toastEl = $("#toast")
+
+addEventListener("error", (e) => {
+ toast(`error: ${e.message || "script failed"}`, true)
+})
const GAP = 10
const FIT_TOLERANCE = 350
@@ -515,8 +522,9 @@ let pinchDist = 0
stage.addEventListener("pointerdown", (e) => {
if (e.target.closest(".nav-zone")) return
+ if (e.target.closest("#import-state")) return
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY })
- stage.setPointerCapture(e.pointerId)
+ try { stage.setPointerCapture(e.pointerId) } catch { /* ignore */ }
hideGhost()
world.style.transition = "none"
if (pointers.size === 2) {
@@ -638,48 +646,123 @@ addEventListener("keydown", (e) => {
addEventListener("resize", center)
-const MIN_READ = 1500
-const splashShownAt = Date.now()
let started = false
-let splashTimer = 0
-function start() {
+function showImportState() {
+ importState.hidden = false
+ stage.classList.add("blocked")
+}
+
+function hideImportState() {
+ importState.hidden = true
+ stage.classList.remove("blocked")
+}
+
+async function beginWalk() {
if (started) return
started = true
- clearTimeout(splashTimer)
- splash.classList.add("hide")
- ;(async () => {
- try {
- const r = await fetch("/api/random")
- const data = await r.json()
- const photo = data.photo
- photoInfo.set(photo.id, photo)
- const a = aspect(photo)
- const wf = a >= 1 ? a : 1
- const hf = a >= 1 ? 1 : 1 / a
- const uW = (window.innerWidth * 0.92) / wf
- const uH = (window.innerHeight * 0.92) / hf
- U = Math.max(100, Math.min(640, Math.round(Math.min(uW, uH))))
- const rect = { x: 0, y: 0, w: tileW(photo), h: tileH(photo) }
- addTile(photo, rect)
- setCurrent(photo.id, rect)
- } catch { /* ignore */ }
- })()
+ try {
+ const r = await fetch("/api/random")
+ const data = await r.json()
+ if (!data.photo) {
+ showImportState()
+ return
+ }
+ hideImportState()
+ const photo = data.photo
+ photoInfo.set(photo.id, photo)
+ const a = aspect(photo)
+ const wf = a >= 1 ? a : 1
+ const hf = a >= 1 ? 1 : 1 / a
+ const uW = (window.innerWidth * 0.92) / wf
+ const uH = (window.innerHeight * 0.92) / hf
+ U = Math.max(100, Math.min(640, Math.round(Math.min(uW, uH))))
+ const rect = { x: 0, y: 0, w: tileW(photo), h: tileH(photo) }
+ addTile(photo, rect)
+ setCurrent(photo.id, rect)
+ } catch { /* ignore */ }
}
-function noteActivity() {
- if (started) return
- const left = MIN_READ - (Date.now() - splashShownAt)
- if (left <= 0) start()
- else {
- clearTimeout(splashTimer)
- splashTimer = setTimeout(start, left)
+;(async () => {
+ try {
+ const r = await fetch("/api/paths")
+ const data = await r.json()
+ if (data.total) await beginWalk()
+ else showImportState()
+ } catch {
+ showImportState()
+ }
+})()
+
+let toastTimer = 0
+
+function toast(msg, isError = false) {
+ toastEl.textContent = msg
+ toastEl.classList.toggle("err", isError)
+ toastEl.classList.add("show")
+ clearTimeout(toastTimer)
+ toastTimer = setTimeout(() => toastEl.classList.remove("show"), isError ? 7000 : 3500)
+}
+
+function plural(n) {
+ return n === 1 ? "photo" : "photos"
+}
+
+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)}...`)
+ 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})`)
+ exitsCache.clear()
+ exitPending.clear()
+ hideImportState()
+ await beginWalk()
+ toast(`imported ${data.imported} ${plural(data.imported)} - keep walking to find them`)
+ } catch (err) {
+ toast(err.message || "import failed", true)
+ } finally {
+ importEl.classList.remove("busy")
+ fileInput.value = ""
}
}
-startBtn.addEventListener("click", start)
-addEventListener("pointermove", noteActivity)
-addEventListener("pointerdown", noteActivity)
-addEventListener("keydown", noteActivity)
-addEventListener("wheel", noteActivity)
-addEventListener("touchstart", noteActivity)
+function hasFiles(e) {
+ return Array.from(e.dataTransfer?.types || []).includes("Files")
+}
+
+
+fileInput.addEventListener("change", () => importFiles(fileInput.files))
+
+let dragDepth = 0
+
+addEventListener("dragenter", (e) => {
+ if (!hasFiles(e)) return
+ e.preventDefault()
+ dragDepth++
+ dropOverlay.classList.add("show")
+})
+
+addEventListener("dragover", (e) => {
+ if (!hasFiles(e)) return
+ e.preventDefault()
+ e.dataTransfer.dropEffect = "copy"
+})
+
+addEventListener("dragleave", () => {
+ dragDepth = Math.max(0, dragDepth - 1)
+ if (dragDepth === 0) dropOverlay.classList.remove("show")
+})
+
+addEventListener("drop", (e) => {
+ if (!e.dataTransfer) return
+ e.preventDefault()
+ dragDepth = 0
+ dropOverlay.classList.remove("show")
+ importFiles(e.dataTransfer.files)
+})