diff --git a/README.md b/README.md index b41ccd9..7aa16d7 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/app.js b/app.js index 24d1774..32fc24b 100644 --- a/app.js +++ b/app.js @@ -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) { diff --git a/index.html b/index.html index 2e0dbf9..0dc3d51 100644 --- a/index.html +++ b/index.html @@ -47,6 +47,24 @@ Reset current build + + + Import / Export + Back up or share the current build as JSON. Paste JSON directly or upload a saved file to import. + + + Generate / Copy JSON + Download JSON + + Upload JSON + + + Import pasted JSON + + + + + Total damage base0 Total health base0 diff --git a/styles.css b/styles.css index d3430a2..d86ff8b 100644 --- a/styles.css +++ b/styles.css @@ -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;
Back up or share the current build as JSON. Paste JSON directly or upload a saved file to import.