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 @@
-
+
+