From 6c3357132946548c318bd42ddb5467e399209848 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:50:29 -0400 Subject: [PATCH] =?UTF-8?q?feat(book):=20JSON=20export=20=E2=80=94=20whole?= =?UTF-8?q?=20book=20+=20cover-only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new payload builders: - bookPayload() — captures spec, pages, masters, cover metadata, every layer that targets a book surface. Hooked into downloadProject so Save Project (JSON) in book mode now writes a real, round- trippable book file instead of falling through to coverTemplatePayload (which silently wrote a malformed DVD-cover JSON for books). - bookCoverPayload() — cover-only payload with the three cover panels + the spec fields the cover needs to lay out correctly (trim, bleed, safe, spine width snapshotted at export so the file stays portable between books with different page counts). Surfaced via a new '💾 Cover JSON' button in the page-edit bar that only shows in cover-edit mode. Co-Authored-By: Claude Opus 4.7 --- src/index.html | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/src/index.html b/src/index.html index 3151fd2..0f801da 100644 --- a/src/index.html +++ b/src/index.html @@ -4921,6 +4921,7 @@ + @@ -12497,6 +12498,8 @@ if (prev) prev.style.display = 'none'; if (next) next.style.display = 'none'; if (tfBtn) tfBtn.style.display = 'none'; + const coverExportBtn = document.getElementById('bookCoverExportJsonBtn'); + if (coverExportBtn) coverExportBtn.style.display = ''; if (pubBtn) { const visTag = state.visibility === 'private' ? ' 🔒' : ''; const ref = book.cover?.ref; @@ -12507,6 +12510,11 @@ } return; } + // Editing a page / master → hide the cover-only export button. + { + const coverExportBtn = document.getElementById('bookCoverExportJsonBtn'); + if (coverExportBtn) coverExportBtn.style.display = 'none'; + } if (editingMaster) { const m = book.masters.find(x => x.id === book.editingMasterId); if (label) label.textContent = `Master: ${m?.label || 'Untitled'}`; @@ -25918,6 +25926,90 @@ }; } + // Whole-book payload — spec + pages + masters + cover metadata + + // every layer that belongs to any book surface (pages, masters, + // cover panels). Transient UI fields (editingCover / editingEntryId + // / editingMasterId / editingChapterId) are excluded; restore opens + // the book at the Pages overview. + function bookPayload() { + const book = state.book || {}; + return { + version: SCHEMA_VERSION, + templateMode: 'book', + templateType: 'book', + meta: state.meta, + ui: { + zoom: state.zoom, + showGuides: state.showGuides, + showBleed: state.showBleed, + showCrops: state.showCrops, + printGuides: state.printGuides, + printCrops: state.printCrops, + }, + book: { + spec: book.spec || {}, + pages: book.pages || [], + masters: book.masters || [], + cover: book.cover || {}, + }, + layerGroups: { ...ensureLayerGroups() }, + layers: nonGeneratedLayers().filter(l => { + const t = String(l.target || ''); + return t.startsWith('book-page:') + || t.startsWith('book-master:') + || t === 'book-cover-front' + || t === 'book-spine' + || t === 'book-cover-back'; + }), + }; + } + + // Cover-only payload — just the three cover panels + the spec + // fields the cover needs to render correctly (trim, bleed, safe, + // spine width snapshotted at export so the file is portable). Use + // it to move a cover spread between books or stash it as a + // standalone artifact. + function bookCoverPayload() { + const spec = state.book?.spec || {}; + return { + version: SCHEMA_VERSION, + templateMode: 'book-cover', + templateType: 'book-cover', + meta: state.meta, + ui: { + zoom: state.zoom, + showGuides: state.showGuides, + showBleed: state.showBleed, + showCrops: state.showCrops, + }, + bookCover: { + trimW: Number(spec.trimW) || 0, + trimH: Number(spec.trimH) || 0, + bleed: Number(spec.bleed) || 0, + safe: Number(spec.safe) || 0.125, + coverHasBack: !!spec.coverHasBack, + coverHasSpine: !!spec.coverHasSpine, + // Snapshot the derived spine width so the cover is + // self-contained — re-importing into a book with a + // different page count won't shift it unless the user opts + // in by toggling spineFromPages. + spineWidth: bookSpineWidthInches(), + spineFromPages: !!spec.spineFromPages, + spineWManual: Number(spec.spineWManual) || 0, + paperThickness: Number(spec.paperThickness) || 0, + pageCountAtExport: bookPageCount(), + bookSize: spec.bookSize || 'Custom', + }, + layerGroups: { ...ensureLayerGroups() }, + layers: nonGeneratedLayers().filter(l => { + const t = String(l.target || ''); + return t === 'book-cover-front' + || t === 'book-spine' + || t === 'book-cover-back'; + }), + }; + } + function exportPayload() { const templateType = templateTypeForMode(); return { @@ -27966,6 +28058,11 @@ } else if (state.templateMode === 'pixelart') { const pa = state.pixelArt || defaultPixelArt(32, 32); triggerDownload(pixelArtPayload(), projectFilename('pixel_art', `${pa.width}x${pa.height}`)); + } else if (state.templateMode === 'book') { + const spec = state.book?.spec || {}; + const variant = filenamePart(spec.bookSize, 'custom') + || `${Number(spec.trimW) || 0}x${Number(spec.trimH) || 0}`; + triggerDownload(bookPayload(), projectFilename('book', variant)); } else if (isCanvasMode(state.templateMode)) { const ca = state.customArt || { width: 1920, height: 1080 }; const stem = state.templateMode === 'slide' ? 'slide' : 'custom_art'; @@ -37146,6 +37243,16 @@ const id = state.book?.editingEntryId; if (id) openBookPageGatingDialog(id); }); + on('bookCoverExportJsonBtn', 'click', () => { + if (!state.book?.editingCover) return; + const titlePart = filenamePart(state.meta?.title, 'untitled'); + const spec = state.book?.spec || {}; + const sizePart = filenamePart(spec.bookSize, '') + || `${Number(spec.trimW) || 0}x${Number(spec.trimH) || 0}`; + const filename = [titlePart, 'book_cover', sizePart].filter(Boolean).join('_') + '.json'; + triggerDownload(bookCoverPayload(), filename); + toastSuccess?.('Cover spread saved.'); + }); on('bookPageEditProofPdf', 'click', async () => { const book = state.book; const entryId = book?.editingEntryId;