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");
|
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
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user