Add JSON import and export
This commit is contained in:
@@ -101,6 +101,17 @@ Features:
|
||||
- Delete a slot
|
||||
- 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
|
||||
|
||||
Open `index.html` in a browser.
|
||||
|
||||
@@ -43,6 +43,7 @@ const PROFILES = {
|
||||
|
||||
const STORAGE_KEY = "forgeMasterOptimizer.v1";
|
||||
const DRAFT_KEY = "forgeMasterOptimizer.draft.v1";
|
||||
const EXPORT_VERSION = 1;
|
||||
const slots = [
|
||||
...Array.from({ length: 8 }, (_, i) => ({ type: "item", title: `Item ${i + 1}` })),
|
||||
{ type: "mount", title: "Mount" },
|
||||
@@ -358,6 +359,100 @@ function setStatus(message, danger = false) {
|
||||
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() {
|
||||
$("#profileSelect").addEventListener("change", (event) => {
|
||||
state.profile = event.target.value;
|
||||
@@ -381,6 +476,10 @@ function bindEvents() {
|
||||
$("#saveSlot").addEventListener("click", saveNamedSlot);
|
||||
$("#loadSlot").addEventListener("click", loadNamedSlot);
|
||||
$("#deleteSlot").addEventListener("click", deleteNamedSlot);
|
||||
$("#copyExport").addEventListener("click", generateExportJson);
|
||||
$("#downloadExport").addEventListener("click", downloadExportJson);
|
||||
$("#importJson").addEventListener("click", importPastedJson);
|
||||
$("#importFile").addEventListener("change", importUploadedJson);
|
||||
}
|
||||
|
||||
function handleGearInput(event) {
|
||||
|
||||
+18
@@ -47,6 +47,24 @@
|
||||
<button id="resetBuild" type="button" class="secondary">Reset current build</button>
|
||||
</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">
|
||||
<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>
|
||||
|
||||
+28
-2
@@ -26,11 +26,11 @@ body {
|
||||
var(--bg);
|
||||
}
|
||||
|
||||
button, input, select {
|
||||
button, input, select, textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button, input, select {
|
||||
button, input, select, textarea {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.75rem;
|
||||
color: var(--text);
|
||||
@@ -48,6 +48,7 @@ button {
|
||||
button:hover { filter: brightness(1.1); }
|
||||
button.secondary { background: var(--panel-2); }
|
||||
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 {
|
||||
display: grid;
|
||||
@@ -104,6 +105,31 @@ h3 { margin-bottom: 0; }
|
||||
}
|
||||
.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 {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
||||
Reference in New Issue
Block a user