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:
+64
-5
@@ -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");
|
||||
|
||||
+2
-1
@@ -36,7 +36,8 @@
|
||||
<button id="undo" title="Undo (⌘Z)">Undo</button>
|
||||
<button id="clear" title="Clear current floor">Clear</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>
|
||||
</header>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user