export/import carries notes + texts; new Import button

- exportJson now includes per-floor notes and texts arrays alongside route/pulls; bumps a version field for forward-compat
- new importJson: file picker, parses JSON, resolves dungeon by id against current build, replaces this dungeon's pins on every floor present in the file
- Import button next to Export in the toolbar
This commit is contained in:
2026-04-25 23:05:48 +02:00
parent 569dbb3b7c
commit f8c5157c80
2 changed files with 66 additions and 6 deletions
+64 -5
View File
@@ -941,16 +941,25 @@ async function shareUrl() {
toast("Link copied"); toast("Link copied");
} }
const EXPORT_VERSION = 1;
function exportJson() { function exportJson() {
const d = state.current; const d = state.current;
if (!d) return; if (!d) return;
const payload = { const payload = {
version: EXPORT_VERSION,
dungeon: { id: d.id, name: d.name }, dungeon: { id: d.id, name: d.name },
floors: d.maps.map((m, i) => ({ floors: d.maps.map((m, i) => {
label: m.label, const k = `${d.id}::${i}`;
route: state.routes[`${d.id}::${i}`] || [], return {
pulls: state.pulls[`${d.id}::${i}`] || [], 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 blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@@ -961,6 +970,54 @@ function exportJson() {
URL.revokeObjectURL(url); 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) { function toast(msg) {
let t = document.querySelector(".toast"); let t = document.querySelector(".toast");
if (!t) { if (!t) {
@@ -989,6 +1046,8 @@ function hookEvents() {
$("clear").addEventListener("click", clearCurrent); $("clear").addEventListener("click", clearCurrent);
$("share").addEventListener("click", shareUrl); $("share").addEventListener("click", shareUrl);
$("export").addEventListener("click", exportJson); $("export").addEventListener("click", exportJson);
const importBtn = $("import");
if (importBtn) importBtn.addEventListener("click", importJson);
$("tool-route").addEventListener("click", () => setTool("route")); $("tool-route").addEventListener("click", () => setTool("route"));
$("tool-pull").addEventListener("click", () => setTool("pull")); $("tool-pull").addEventListener("click", () => setTool("pull"));
const noteBtn = $("tool-note"); const noteBtn = $("tool-note");
+2 -1
View File
@@ -36,7 +36,8 @@
<button id="undo" title="Undo (⌘Z)">Undo</button> <button id="undo" title="Undo (⌘Z)">Undo</button>
<button id="clear" title="Clear current floor">Clear</button> <button id="clear" title="Clear current floor">Clear</button>
<button id="share">Share</button> <button id="share">Share</button>
<button id="export">Export JSON</button> <button id="export" title="Download this dungeon's routes/pulls/notes/labels as JSON">Export</button>
<button id="import" title="Load a previously exported route file">Import</button>
</div> </div>
</header> </header>