mirror of
https://github.com/PR0M3TH3AN/Artstr-Studio.git
synced 2026-10-27 08:40:39 +00:00
External embed Phase 4: deck player with keyboard + fullscreen
Public multi-slide deck embeds get a paginated viewer instead of
the single-thumbnail-with-badge that was hitting before (the
shared renderDeckPreviewDOM compact path).
_paintEmbed now branches on mode === 'deck' && slides > 1 into a
new _paintEmbedDeck that:
- Reads ?slide=N (1-based) from the route params for the initial
index; clamps to [0, slides.length-1].
- Renders the current slide via composeSlideForDeck +
renderDesignPayloadPreview into a .designObjectFitWrap and
fitMiniPreview-scales it.
- Adds prev / counter / next pill chips to the footer left side
(only when slides > 1 and controls=1), and the existing author
chip after them.
- Adds a fullscreen chip ⛶ + the Open-in-Artstr link to the
footer right side.
- Wires keyboard nav on document.keydown:
ArrowRight / PageDown / Space → next
ArrowLeft / PageUp → prev
Home → first; End → last
F → toggle fullscreen
Single handler is swapped via the _paintEmbedDeck._kbd sentinel
so re-renders (e.g., the refresh-error path) don't stack
listeners. Form-element targets (none today, future-proof) are
ignored so the embed doesn't eat typing.
- New _toggleEmbedFullscreen helper calls
documentElement.requestFullscreen() / exitFullscreen with a
graceful catch.
Touch swipe, autoplay (?autoplay / ?loop / ?interval), and
prefers-reduced-motion respect are deferred — they fit naturally
on top of this scaffold whenever we want them.
This commit is contained in:
1 parent
425e734238
commit
d29afaea8c
1 file changed
+148
+148
@@ -21436,7 +21436,155 @@
|
||||
|
||||
// Paint the resolved payload into the chromeless embed stage
|
||||
// and populate the footer (author + "Open in Artstr" link).
|
||||
// Deck embed player (EXTERNAL_EMBED_FEATURE.md §11.3 / §18 Phase 4).
|
||||
// Multi-slide decks paginate inside the iframe with prev/next +
|
||||
// counter chips in the footer, keyboard nav, and a fullscreen
|
||||
// toggle. ?slide=N (1-based) deep-links to a starting slide.
|
||||
function _paintEmbedDeck(root, payload, event, route) {
|
||||
const stage = root.querySelector('.embedStage');
|
||||
const footer = root.querySelector('.embedFooter');
|
||||
if (!stage || !footer) return;
|
||||
const slides = payload.deck?.slides || [];
|
||||
const theme = payload.deck?.theme || {};
|
||||
const params = route.params || new URLSearchParams();
|
||||
const requested = Math.max(1, parseInt(params.get('slide') || '1', 10)) - 1;
|
||||
let idx = Math.min(slides.length - 1, Math.max(0, requested));
|
||||
stage.innerHTML = '';
|
||||
footer.innerHTML = '';
|
||||
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'designObjectFitWrap';
|
||||
wrap.style.cssText = 'position:relative;width:100%;height:100%;overflow:hidden;display:flex;align-items:center;justify-content:center';
|
||||
stage.appendChild(wrap);
|
||||
|
||||
const showControls = params.get('controls') !== '0';
|
||||
const showAttr = params.get('attr') !== '0';
|
||||
const showOpen = params.get('open') !== '0';
|
||||
|
||||
const left = document.createElement('div');
|
||||
left.className = 'embedSide';
|
||||
const right = document.createElement('div');
|
||||
right.className = 'embedSide';
|
||||
right.style.justifyContent = 'flex-end';
|
||||
|
||||
let counter = null;
|
||||
let prevBtn = null;
|
||||
let nextBtn = null;
|
||||
|
||||
const paintSlide = () => {
|
||||
wrap.innerHTML = '';
|
||||
const composed = composeSlideForDeck(slides[idx], theme);
|
||||
try {
|
||||
renderDesignPayloadPreview(wrap, composed, { compact: true, transparentPixelArt: true });
|
||||
fitMiniPreview(wrap);
|
||||
} catch (err) {
|
||||
console.warn('[embed] deck slide render failed', err);
|
||||
wrap.innerHTML = '<div class="embedStatus error">Slide preview unavailable.</div>';
|
||||
}
|
||||
if (counter) counter.textContent = `${idx + 1} / ${slides.length}`;
|
||||
};
|
||||
|
||||
const go = (newIdx) => {
|
||||
idx = Math.min(slides.length - 1, Math.max(0, newIdx));
|
||||
paintSlide();
|
||||
};
|
||||
|
||||
if (showControls && slides.length > 1) {
|
||||
prevBtn = document.createElement('button');
|
||||
prevBtn.type = 'button';
|
||||
prevBtn.className = 'embedChip';
|
||||
prevBtn.textContent = '◂';
|
||||
prevBtn.title = 'Previous slide';
|
||||
prevBtn.addEventListener('click', () => go((idx - 1 + slides.length) % slides.length));
|
||||
counter = document.createElement('span');
|
||||
counter.className = 'embedChip embedChipCounter';
|
||||
nextBtn = document.createElement('button');
|
||||
nextBtn.type = 'button';
|
||||
nextBtn.className = 'embedChip';
|
||||
nextBtn.textContent = '▸';
|
||||
nextBtn.title = 'Next slide';
|
||||
nextBtn.addEventListener('click', () => go((idx + 1) % slides.length));
|
||||
left.append(prevBtn, counter, nextBtn);
|
||||
}
|
||||
if (showAttr && event?.pubkey) {
|
||||
left.appendChild(_makeAuthorChip(event.pubkey));
|
||||
}
|
||||
|
||||
if (showControls) {
|
||||
const fsBtn = document.createElement('button');
|
||||
fsBtn.type = 'button';
|
||||
fsBtn.className = 'embedChip';
|
||||
fsBtn.textContent = '⛶';
|
||||
fsBtn.title = 'Toggle fullscreen';
|
||||
fsBtn.addEventListener('click', () => _toggleEmbedFullscreen(root));
|
||||
right.appendChild(fsBtn);
|
||||
}
|
||||
if (showOpen) {
|
||||
const openLink = document.createElement('a');
|
||||
openLink.className = 'embedChip';
|
||||
openLink.textContent = 'Open in Artstr ↗';
|
||||
openLink.href = `${location.origin}/#/share/${route.id}`;
|
||||
openLink.target = '_top';
|
||||
openLink.rel = 'noopener';
|
||||
right.appendChild(openLink);
|
||||
}
|
||||
footer.appendChild(left);
|
||||
footer.appendChild(right);
|
||||
footer.dataset.hidden = (!showControls && !showAttr && !showOpen) ? 'true' : 'false';
|
||||
|
||||
// Keyboard navigation. Re-attaches on every _paintEmbedDeck
|
||||
// call (e.g., the Refresh button on an error state), so swap
|
||||
// any prior handler off first to avoid double-firing.
|
||||
if (_paintEmbedDeck._kbd) {
|
||||
document.removeEventListener('keydown', _paintEmbedDeck._kbd);
|
||||
}
|
||||
const onKey = (e) => {
|
||||
const tag = (e.target?.tagName || '').toLowerCase();
|
||||
if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
case 'PageDown':
|
||||
case ' ':
|
||||
e.preventDefault(); go((idx + 1) % slides.length); break;
|
||||
case 'ArrowLeft':
|
||||
case 'PageUp':
|
||||
e.preventDefault(); go((idx - 1 + slides.length) % slides.length); break;
|
||||
case 'Home':
|
||||
e.preventDefault(); go(0); break;
|
||||
case 'End':
|
||||
e.preventDefault(); go(slides.length - 1); break;
|
||||
case 'f':
|
||||
case 'F':
|
||||
e.preventDefault(); _toggleEmbedFullscreen(root); break;
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', onKey);
|
||||
_paintEmbedDeck._kbd = onKey;
|
||||
|
||||
paintSlide();
|
||||
}
|
||||
|
||||
function _toggleEmbedFullscreen(root) {
|
||||
const target = root || document.documentElement;
|
||||
if (document.fullscreenElement) {
|
||||
document.exitFullscreen?.().catch(() => {});
|
||||
} else {
|
||||
target.requestFullscreen?.().catch((err) => {
|
||||
console.warn('[embed] fullscreen request failed', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _paintEmbed(root, payload, event, route, decoded) {
|
||||
// Deck payloads with more than one slide get a paginated
|
||||
// player (Phase 4). Single-slide decks fall through to the
|
||||
// standard single-design paint path.
|
||||
const mode = (typeof normalizeTemplateMode === 'function')
|
||||
? normalizeTemplateMode(payload) : payload?.templateMode;
|
||||
if (mode === 'deck' && Array.isArray(payload?.deck?.slides) && payload.deck.slides.length > 1) {
|
||||
_paintEmbedDeck(root, payload, event, route);
|
||||
return;
|
||||
}
|
||||
const stage = root.querySelector('.embedStage');
|
||||
const footer = root.querySelector('.embedFooter');
|
||||
if (!stage || !footer) return;
|
||||
|
||||
Reference in new issue
Block a user