Add JSON import and export

This commit is contained in:
Jan Bader
2026-06-16 15:12:12 +02:00
parent 690faf6c57
commit 02e63d10dd
4 changed files with 156 additions and 2 deletions
+11
View File
@@ -101,6 +101,17 @@ Features:
- Delete a slot - Delete a slot
- Automatic draft saving for the current unsaved work - Automatic draft saving for the current unsaved work
## Import and export
The current build can be backed up or shared as JSON:
- Generate JSON in the text box and copy it.
- Download the current build as a `.json` file.
- Import by pasting JSON into the text box.
- Import by uploading a `.json` file.
Imports accept both the app's wrapped export format and the raw build object. Imported builds are normalized, rendered immediately, and autosaved as the current draft.
## Running locally ## Running locally
Open `index.html` in a browser. Open `index.html` in a browser.
+99
View File
@@ -43,6 +43,7 @@ const PROFILES = {
const STORAGE_KEY = "forgeMasterOptimizer.v1"; const STORAGE_KEY = "forgeMasterOptimizer.v1";
const DRAFT_KEY = "forgeMasterOptimizer.draft.v1"; const DRAFT_KEY = "forgeMasterOptimizer.draft.v1";
const EXPORT_VERSION = 1;
const slots = [ const slots = [
...Array.from({ length: 8 }, (_, i) => ({ type: "item", title: `Item ${i + 1}` })), ...Array.from({ length: 8 }, (_, i) => ({ type: "item", title: `Item ${i + 1}` })),
{ type: "mount", title: "Mount" }, { type: "mount", title: "Mount" },
@@ -358,6 +359,100 @@ function setStatus(message, danger = false) {
node.style.color = danger ? "var(--danger)" : "var(--good)"; node.style.color = danger ? "var(--danger)" : "var(--good)";
} }
function setImportExportStatus(message, danger = false) {
const node = $("#importExportStatus");
node.textContent = message;
node.style.color = danger ? "var(--danger)" : "var(--good)";
}
function createExportPayload() {
return {
app: "forge-master-strategy-optimizer",
version: EXPORT_VERSION,
exportedAt: new Date().toISOString(),
build: normalizeState(state),
};
}
function createExportJson() {
return JSON.stringify(createExportPayload(), null, 2);
}
async function generateExportJson() {
const json = createExportJson();
const textarea = $("#jsonTransfer");
textarea.value = json;
textarea.focus();
textarea.select();
try {
await navigator.clipboard.writeText(json);
setImportExportStatus("Export JSON generated and copied to clipboard.");
} catch {
setImportExportStatus("Export JSON generated. Copy it from the text box or download it as a file.");
}
}
function downloadExportJson() {
const json = createExportJson();
$("#jsonTransfer").value = json;
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `forge-master-build-${new Date().toISOString().slice(0, 10)}.json`;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(url);
setImportExportStatus("Downloaded current build as JSON.");
}
function parseImportedState(jsonText) {
if (!jsonText.trim()) throw new Error("Paste JSON first or upload a JSON file.");
let parsed;
try {
parsed = JSON.parse(jsonText);
} catch {
throw new Error("Invalid JSON. Check that the full export was copied.");
}
const build = parsed?.build || parsed;
if (!build || typeof build !== "object" || !Array.isArray(build.pieces)) {
throw new Error("JSON does not look like a Forge Master build export.");
}
return normalizeState(build);
}
function importJsonText(jsonText, sourceLabel = "JSON") {
try {
state = parseImportedState(jsonText);
render();
setImportExportStatus(`Imported ${sourceLabel}. Draft autosaved.`);
} catch (error) {
setImportExportStatus(error.message, true);
}
}
function importPastedJson() {
importJsonText($("#jsonTransfer").value, "pasted JSON");
}
function importUploadedJson(event) {
const file = event.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.addEventListener("load", () => {
const text = String(reader.result || "");
$("#jsonTransfer").value = text;
importJsonText(text, file.name);
event.target.value = "";
});
reader.addEventListener("error", () => {
setImportExportStatus("Could not read the selected file.", true);
event.target.value = "";
});
reader.readAsText(file);
}
function bindEvents() { function bindEvents() {
$("#profileSelect").addEventListener("change", (event) => { $("#profileSelect").addEventListener("change", (event) => {
state.profile = event.target.value; state.profile = event.target.value;
@@ -381,6 +476,10 @@ function bindEvents() {
$("#saveSlot").addEventListener("click", saveNamedSlot); $("#saveSlot").addEventListener("click", saveNamedSlot);
$("#loadSlot").addEventListener("click", loadNamedSlot); $("#loadSlot").addEventListener("click", loadNamedSlot);
$("#deleteSlot").addEventListener("click", deleteNamedSlot); $("#deleteSlot").addEventListener("click", deleteNamedSlot);
$("#copyExport").addEventListener("click", generateExportJson);
$("#downloadExport").addEventListener("click", downloadExportJson);
$("#importJson").addEventListener("click", importPastedJson);
$("#importFile").addEventListener("change", importUploadedJson);
} }
function handleGearInput(event) { function handleGearInput(event) {
+18
View File
@@ -47,6 +47,24 @@
<button id="resetBuild" type="button" class="secondary">Reset current build</button> <button id="resetBuild" type="button" class="secondary">Reset current build</button>
</section> </section>
<section class="card import-export" aria-labelledby="import-export-heading">
<div>
<h2 id="import-export-heading">Import / Export</h2>
<p>Back up or share the current build as JSON. Paste JSON directly or upload a saved file to import.</p>
</div>
<div class="io-actions">
<button id="copyExport" type="button">Generate / Copy JSON</button>
<button id="downloadExport" type="button" class="secondary">Download JSON</button>
<label class="file-button secondary">
Upload JSON
<input id="importFile" type="file" accept="application/json,.json" />
</label>
<button id="importJson" type="button">Import pasted JSON</button>
</div>
<textarea id="jsonTransfer" rows="8" spellcheck="false" placeholder="Generated export JSON appears here. You can also paste JSON here and click Import pasted JSON."></textarea>
<p id="importExportStatus" class="status" role="status"></p>
</section>
<section class="summary-grid" aria-label="Build summary"> <section class="summary-grid" aria-label="Build summary">
<article class="metric"><span>Total damage base</span><strong id="totalDamage">0</strong></article> <article class="metric"><span>Total damage base</span><strong id="totalDamage">0</strong></article>
<article class="metric"><span>Total health base</span><strong id="totalHealth">0</strong></article> <article class="metric"><span>Total health base</span><strong id="totalHealth">0</strong></article>
+28 -2
View File
@@ -26,11 +26,11 @@ body {
var(--bg); var(--bg);
} }
button, input, select { button, input, select, textarea {
font: inherit; font: inherit;
} }
button, input, select { button, input, select, textarea {
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 0.75rem; border-radius: 0.75rem;
color: var(--text); color: var(--text);
@@ -48,6 +48,7 @@ button {
button:hover { filter: brightness(1.1); } button:hover { filter: brightness(1.1); }
button.secondary { background: var(--panel-2); } button.secondary { background: var(--panel-2); }
button.danger { border-color: rgba(251, 113, 133, 0.5); background: rgba(251, 113, 133, 0.16); } button.danger { border-color: rgba(251, 113, 133, 0.5); background: rgba(251, 113, 133, 0.16); }
textarea { width: 100%; min-height: 9rem; resize: vertical; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
label { label {
display: grid; display: grid;
@@ -104,6 +105,31 @@ h3 { margin-bottom: 0; }
} }
.controls label { min-width: 220px; } .controls label { min-width: 220px; }
.import-export {
display: grid;
gap: 0.9rem;
margin-bottom: 1rem;
}
.import-export p { color: var(--muted); margin-bottom: 0; }
.io-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.file-button {
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: 1px solid var(--border);
border-radius: 0.75rem;
color: var(--text);
background: var(--panel-2);
padding: 0.7rem 0.85rem;
font-weight: 700;
}
.file-button input { display: none; }
.summary-grid, .results, .gear-grid { .summary-grid, .results, .gear-grid {
display: grid; display: grid;
gap: 1rem; gap: 1rem;