From 8579789ddb760b9bfa67effcad5f0d91cdfb5bef Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 30 May 2026 23:45:09 -0400 Subject: [PATCH] Premium publishing: support 'deck' template mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A user trying to publish a premium slide deck hit "Premium preview not supported for mode 'deck'." — the publish flow couldn't render the watermarked preview because the mode-dispatch in _renderModeToExportCanvas had cases for cover / disc / jewel / customart / slide / pixelart / disc-design but not deck. Fix: - Add a deck branch to _premiumPreviewLongAxisInches that pulls width / height from state.deck.slides[0].slide (matches the customart / slide path: pixels → inches at 96 dpi). - Add a deck branch to _renderModeToExportCanvas that dispatches to a new renderDeckToExportCanvas. - renderDeckToExportCanvas mirrors renderCustomArtToExportCanvas but pulls the slide payload from the deck. Uses the currently- edited slide when deck.editingIndex is set (matches what the user sees while composing); otherwise falls back to slide 1 (the deck-thumbnail convention). composeSlideForDeck applies the deck theme so the preview shows the buyer what they'll actually unlock. Empty decks emit a blank canvas rather than throwing — the publish modal already gates on having content, so this is just a defensive fallback. With this commit every template mode supports premium publishing — including decks — so a creator can charge sats for a slide deck the same way they can for a cover or custom art. Decryption side is mode-agnostic (Premium.decryptPayload just returns the JSON, which loadProjectFromText already routes to the deck builder), so no buyer-side change needed. --- src/index.html | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/index.html b/src/index.html index f88535a..a4bf9de 100644 --- a/src/index.html +++ b/src/index.html @@ -17846,6 +17846,15 @@ const h = (state.customArt?.height || 1080) / 96; return Math.max(w, h); } + if (mode === 'deck') { + // The preview renders the deck's first slide. Deck slides + // default to 1920x1080 = 20x11.25in like customart / slide. + const slides = state.deck?.slides || []; + const sl = slides[0]?.slide || {}; + const w = (Number(sl.width) || 1920) / 96; + const h = (Number(sl.height) || 1080) / 96; + return Math.max(w, h); + } // disc-sheet, jewel: letter-sized return 11; } @@ -17857,9 +17866,50 @@ if (mode === 'jewel') return renderJewelSheetToExportCanvas(dpi); if (mode === 'customart' || mode === 'slide') return renderCustomArtToExportCanvas(dpi); if (mode === 'pixelart') return renderPixelArtToExportCanvas(); + if (mode === 'deck') return renderDeckToExportCanvas(dpi); throw new Error(`Premium preview not supported for mode "${mode}".`); } + // Render the deck's first slide (themed via composeSlideForDeck) + // as the premium preview source. This is the slide buyers see + // before unlocking — using slide 1 matches the deck-thumbnail + // convention. Decks with zero slides produce an empty canvas + // rather than throwing so the publish modal can still proceed. + async function renderDeckToExportCanvas(dpi) { + const deck = state.deck || {}; + const slides = Array.isArray(deck.slides) ? deck.slides : []; + // Prefer the currently-edited slide if the user is mid-edit; + // otherwise slide 1. + let entry = null; + if (deck.editingIndex != null && slides[deck.editingIndex]) { + entry = slides[deck.editingIndex]; + } else if (slides[0]) { + entry = slides[0]; + } + if (!entry) { + const canvas = document.createElement('canvas'); + setupExportCanvas(canvas, 20, 11.25, dpi); + return canvas; + } + const composed = composeSlideForDeck(entry, deck.theme || {}); + const sl = composed.slide || {}; + const wIn = (Number(sl.width) || 1920) / 96; + const hIn = (Number(sl.height) || 1080) / 96; + const canvas = document.createElement('canvas'); + const ctx = setupExportCanvas(canvas, wIn, hIn, dpi); + const bg = sl.background || '#ffffff'; + if (bg && bg.toLowerCase() !== '#ffffff' && bg.toLowerCase() !== '#fff') { + ctx.fillStyle = bg; + ctx.fillRect(0, 0, canvas.width, canvas.height); + } + const layers = (composed.layers || []) + .filter(l => (l.target === 'canvas' || !l.target) && !l.hidden && !l._generatedDiscLayer) + .slice() + .sort((a, b) => (a.z || 0) - (b.z || 0)); + for (const layer of layers) await drawLayerToCanvas(ctx, layer, dpi); + return canvas; + } + // Render the standalone pixel-art design (state.pixelArt frame 0) into // a canvas suitable for the premium watermarked preview. Native pixel // dimensions are tiny (32-128 px), so scale up by the largest integer