diff --git a/web/app.js b/web/app.js index a47fd7b..f558f1f 100644 --- a/web/app.js +++ b/web/app.js @@ -941,16 +941,25 @@ async function shareUrl() { toast("Link copied"); } +const EXPORT_VERSION = 1; + function exportJson() { const d = state.current; if (!d) return; const payload = { + version: EXPORT_VERSION, dungeon: { id: d.id, name: d.name }, - floors: d.maps.map((m, i) => ({ - label: m.label, - route: state.routes[`${d.id}::${i}`] || [], - pulls: state.pulls[`${d.id}::${i}`] || [], - })), + floors: d.maps.map((m, i) => { + const k = `${d.id}::${i}`; + return { + index: i, + label: m.label, + route: state.routes[k] || [], + pulls: state.pulls[k] || [], + notes: state.notes[k] || [], + texts: state.texts[k] || [], + }; + }), }; const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" }); const url = URL.createObjectURL(blob); @@ -961,6 +970,54 @@ function exportJson() { URL.revokeObjectURL(url); } +function importJson() { + const input = document.createElement("input"); + input.type = "file"; + input.accept = "application/json,.json"; + input.onchange = async () => { + const file = input.files?.[0]; + if (!file) return; + let payload; + try { + const text = await file.text(); + payload = JSON.parse(text); + } catch (e) { + alert("Could not parse JSON: " + e.message); + return; + } + if (!payload || !payload.dungeon || !payload.dungeon.id || !Array.isArray(payload.floors)) { + alert("Not a recognized route file (missing dungeon/floors)."); + return; + } + const d = state.dungeons.find((x) => x.id === payload.dungeon.id); + if (!d) { + alert(`Dungeon "${payload.dungeon.id}" is not in this build's data set.`); + return; + } + // Replace this dungeon's data on every floor present in the file. + const cleanPin = (p) => ({ x: Math.round(p.x), y: Math.round(p.y) }); + const cleanNote = (n) => ({ + x: Math.round(n.x), y: Math.round(n.y), text: String(n.text ?? ""), + }); + for (const f of payload.floors) { + const i = Number.isInteger(f.index) ? f.index : 0; + const k = `${d.id}::${i}`; + state.routes[k] = (f.route || []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map(cleanPin); + state.pulls[k] = (f.pulls || []).filter((p) => p && Number.isFinite(p.x) && Number.isFinite(p.y)).map(cleanPin); + state.notes[k] = (f.notes || []).filter((n) => n && Number.isFinite(n.x) && Number.isFinite(n.y)).map(cleanNote); + state.texts[k] = (f.texts || []).filter((n) => n && Number.isFinite(n.x) && Number.isFinite(n.y)).map(cleanNote); + } + state.current = d; + state.floorIndex = 0; + pushHistory(); + renderDungeonList(); + renderViewer(); + updateHash(); + toast(`Imported ${payload.floors.length} floor${payload.floors.length === 1 ? "" : "s"} of ${d.name}`); + }; + input.click(); +} + function toast(msg) { let t = document.querySelector(".toast"); if (!t) { @@ -989,6 +1046,8 @@ function hookEvents() { $("clear").addEventListener("click", clearCurrent); $("share").addEventListener("click", shareUrl); $("export").addEventListener("click", exportJson); + const importBtn = $("import"); + if (importBtn) importBtn.addEventListener("click", importJson); $("tool-route").addEventListener("click", () => setTool("route")); $("tool-pull").addEventListener("click", () => setTool("pull")); const noteBtn = $("tool-note"); diff --git a/web/index.html b/web/index.html index 54d601a..2f8ea17 100644 --- a/web/index.html +++ b/web/index.html @@ -36,7 +36,8 @@ - + +