Files
pulse-signage/src/player/render.js
T
2026-08-03 19:18:30 +01:00

169 lines
8.1 KiB
JavaScript

// Player page rendering and bootstrap script assembly.
const Handlebars = require('handlebars');
const path = require('path');
const { mediaKind, safeJsonForScript, getPlayerPageTemplate, getPlayerClientNameScript, getPlayerPageOfflineScript, getPlayerPagePlaylistScript, getPlayerPageCommandsScript, getPlayerPageRenderingScript, getPlayerPagePlaybackScript, getAnnouncementIconsDataScript, getPlayerAnnouncementTemplatesDataScript, getPlayerAnnouncementTemplatesScript, getPlayerPageAnnouncementsScript, getPlayerPageScript, getPlayerRegionScripts, getPlayerOnboardingLandingScript, getPlayerOnboardingFormScript } = require('./render-helpers');
const { createThumbnailPreviewBootstrapScript } = require('./thumbnail-preview');
const { createPageAuthBundle, createPageFetchAuthScript } = require('#src/request-auth');
const { getFontStylesheetHref } = require('#src/web/lib/media/font-library');
const PLAYER_MEDIA_DIR = path.join(__dirname, '..', '..', 'media');
function renderPage(template, options) {
const stylesheetLinks = Array.isArray(options && options.stylesheets) ? options.stylesheets : [];
return template({
TITLE: options.title,
BODY_CLASS: options.bodyClass || '',
BODY: new Handlebars.SafeString(options.body || ''),
STYLESHEETS: new Handlebars.SafeString(stylesheetLinks.map(function (href) {
return `<link rel="stylesheet" href="${Handlebars.escapeExpression(href)}" />`;
}).join('')),
SCRIPT_BLOCK: new Handlebars.SafeString(options.script || '')
});
}
function getPlayerServiceWorkerRegistrationScript() {
return [
'<script>',
' if ("serviceWorker" in navigator) {',
' window.addEventListener("load", function () {',
' navigator.serviceWorker.register("/sw.js?v=38").catch(function () {',
' return null;',
' });',
' });',
' }',
'</script>'
].join('');
}
function renderOnboardingLandingBody() {
return [
'<main class="onboarding-shell">',
' <section class="onboarding-card onboarding-card--landing">',
' <p class="onboarding-kicker">Pulse Signage</p>',
' <h1>Onboard this player</h1>',
' <p class="onboarding-copy">Choose an existing screen, name the client, and either scan the QR code or finish right here with a keyboard and mouse.</p>',
' <div class="onboarding-layout">',
' <div class="onboarding-qr-pane">',
' <div class="onboarding-qr-frame">',
' <img id="onboarding-qr" alt="Onboarding QR code" />',
' </div>',
' <div id="onboarding-status" class="onboarding-status">Preparing onboarding link...</div>',
' </div>',
' <form id="onboarding-local-form" class="onboarding-form onboarding-form--local">',
' <label>',
' <span>Client name</span>',
' <input name="clientName" type="text" maxlength="255" required placeholder="Lobby player" autocomplete="off" />',
' </label>',
' <label>',
' <span>Screen</span>',
' <select name="screenSlug" id="onboarding-screen-select" required>',
' <option value="">Loading screens...</option>',
' </select>',
' </label>',
' <button type="submit">Save client</button>',
' <div id="onboarding-message" class="onboarding-status"></div>',
' </form>',
' </div>',
' </section>',
'</main>'
].join('');
}
function renderOnboardingFormBody(deviceId) {
return [
'<main class="onboarding-shell">',
' <section class="onboarding-card onboarding-card--form">',
' <p class="onboarding-kicker">Pulse Signage</p>',
' <h1>Name this client</h1>',
' <p class="onboarding-copy">Pick an existing screen and give this player a friendly name that will persist after refreshes.</p>',
' <form id="onboarding-form" class="onboarding-form">',
' <label>',
' <span>Client name</span>',
' <input name="clientName" type="text" maxlength="255" required placeholder="Lobby player" />',
' </label>',
' <label>',
' <span>Screen</span>',
' <select name="screenSlug" id="onboarding-screen-select" required>',
' <option value="">Loading screens...</option>',
' </select>',
' </label>',
' <input type="hidden" name="deviceId" value="' + Handlebars.escapeExpression(deviceId || '') + '" />',
' <button type="submit">Save client</button>',
' <div id="onboarding-message" class="onboarding-status"></div>',
' </form>',
' </section>',
'</main>'
].join('');
}
function renderOnboardingLandingScript() {
return getPlayerOnboardingLandingScript()();
}
function renderOnboardingFormScript(deviceId) {
return getPlayerOnboardingFormScript()({
DEVICE_ID_JSON: new Handlebars.SafeString(JSON.stringify(deviceId || ''))
});
}
function renderPlayerPage(slug, initialData) {
const onboardingScript = getPlayerClientNameScript()();
const offlineScript = getPlayerPageOfflineScript()();
const playlistScript = getPlayerPagePlaylistScript()();
const commandScript = getPlayerPageCommandsScript()();
const renderingScript = getPlayerPageRenderingScript()();
const playbackScript = getPlayerPagePlaybackScript()();
const serviceWorkerScript = getPlayerServiceWorkerRegistrationScript();
const template = getPlayerPageTemplate();
const hlsScriptTag = '<script src="/assets/vendor/hls.min.js"></script>';
const pageAuthToken = createPageAuthBundle({ scope: 'player', slug: String(slug || '').trim() });
const fontStylesheetHref = getFontStylesheetHref(PLAYER_MEDIA_DIR);
const bodyClass = [initialData && initialData.thumbnailPreview ? 'thumbnail-preview' : '', ''].join(' ').trim();
const script = getPlayerPageScript()({
SLUG_JSON: new Handlebars.SafeString(JSON.stringify(slug)),
INITIAL_DATA_JSON: new Handlebars.SafeString(safeJsonForScript(initialData || null)),
REGION_SCRIPTS: new Handlebars.SafeString(getPlayerRegionScripts())
});
return renderPage(template, {
title: 'Screen ' + slug,
bodyClass: bodyClass,
body: '<div id="app"><div class="empty">Loading screen...</div></div>',
stylesheets: fontStylesheetHref ? [fontStylesheetHref] : [],
script: createPageFetchAuthScript(pageAuthToken, '/ws/screens/' + encodeURIComponent(slug || '')) + hlsScriptTag + serviceWorkerScript + createThumbnailPreviewBootstrapScript(initialData) + '<script>' + offlineScript + '</script>' + '<script>' + playlistScript + '</script>' + '<script>' + commandScript + '</script>' + '<script>' + renderingScript + '</script>' + '<script>' + playbackScript + '</script>' + '<script>' + getAnnouncementIconsDataScript() + '</script>' + '<script>' + getPlayerAnnouncementTemplatesDataScript() + '</script>' + '<script>' + getPlayerAnnouncementTemplatesScript() + '</script>' + onboardingScript + script + '<script>' + getPlayerPageAnnouncementsScript()() + '</script>'
});
}
function renderPlayerOnboardingLandingPage() {
const pageAuthToken = createPageAuthBundle({ scope: 'onboarding' });
const fontStylesheetHref = getFontStylesheetHref(PLAYER_MEDIA_DIR);
return renderPage(getPlayerPageTemplate(), {
title: 'Onboard player',
bodyClass: 'onboarding-page',
body: renderOnboardingLandingBody(),
stylesheets: fontStylesheetHref ? [fontStylesheetHref] : [],
script: createPageFetchAuthScript(pageAuthToken) + getPlayerServiceWorkerRegistrationScript() + renderOnboardingLandingScript()
});
}
function renderPlayerOnboardingFormPage(deviceId) {
const pageAuthToken = createPageAuthBundle({ scope: 'onboarding', deviceId: String(deviceId || '').trim() });
const fontStylesheetHref = getFontStylesheetHref(PLAYER_MEDIA_DIR);
return renderPage(getPlayerPageTemplate(), {
title: 'Onboard screen',
bodyClass: 'onboarding-page',
body: renderOnboardingFormBody(deviceId),
stylesheets: fontStylesheetHref ? [fontStylesheetHref] : [],
script: createPageFetchAuthScript(pageAuthToken) + getPlayerServiceWorkerRegistrationScript() + renderOnboardingFormScript(deviceId)
});
}
module.exports = {
mediaKind,
renderPlayerPage,
renderPlayerOnboardingLandingPage,
renderPlayerOnboardingFormPage
};