Premium publishing: support 'deck' template mode

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.
This commit is contained in:
thePR0M3TH3AN committed 2026-05-30 23:45:09 -04:00
1 parent f957adb99a
commit 8579789ddb
1 file changed
+50
+50
View File
@@ -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