Files
pulse-signage/src/player/render.js
T
lzstealth 8c0c22156e
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m15s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 34s
Release v2.10.6
2026-08-29 14:59:06 +01:00

167 lines
8.3 KiB
JavaScript

// Player page rendering and bootstrap script assembly.
const Handlebars = require('handlebars');
const path = require('path');
const packageMetadata = require('../../package.json');
const { mediaKind, safeJsonForScript, getPlayerPageTemplate, getPlayerPageAnnouncementsScript, getPlayerPageScript, getPlayerOnboardingLandingScript, getPlayerOnboardingFormScript, getPlayerRuntimeScripts } = require('./render-helpers');
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() {
const releaseVersion = encodeURIComponent(String(packageMetadata.version || 'development'));
return [
'<script>',
' if ("serviceWorker" in navigator) {',
' window.addEventListener("load", function () {',
' navigator.serviceWorker.register("/sw.js?v=' + releaseVersion + '").catch(function () {',
' return null;',
' });',
' });',
' }',
'</script>'
].join('');
}
function renderOnboardingLandingBody(options) {
const onboardingCode = String(options && options.pairingCode || '').trim();
const deviceId = String(options && options.deviceId || '').trim();
return [
' <main id="onboarding-shell" class="onboarding-shell">',
' <section class="onboarding-stage onboarding-stage--landing">',
' <div class="onboarding-layout">',
' <div class="onboarding-qr-pane">',
' <div class="onboarding-qr-frame">',
' <img id="onboarding-qr" alt="Onboarding QR code" src="data:image/svg+xml;charset=utf-8,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 320 320%22%3E%3Crect width=%22320%22 height=%22320%22 rx=%2224%22 fill=%22%23ffffff%22/%3E%3Crect x=%2230%22 y=%2230%22 width=%22260%22 height=%22260%22 rx=%2218%22 fill=%22%23f8fafc%22 stroke=%22%23cbd5e1%22 stroke-width=%223%22 stroke-dasharray=%2212 10%22/%3E%3Cpath d=%22M106 118h108M106 156h108M106 194h72%22 stroke=%22%2394a3b8%22 stroke-width=%2214%22 stroke-linecap=%22round%22/%3E%3Ccircle cx=%22128%22 cy=%22248%22 r=%2212%22 fill=%22%2394a3b8%22/%3E%3Ctext x=%22160%22 y=%2278%22 text-anchor=%22middle%22 fill=%22%230f172a%22 font-family=%22Arial,sans-serif%22 font-size=%2224%22 font-weight=%22700%22%3EQR code loading%3C/text%3E%3Ctext x=%22160%22 y=%22266%22 text-anchor=%22middle%22 fill=%22%234b5563%22 font-family=%22Arial,sans-serif%22 font-size=%2214%22%3EPlease wait%3C/text%3E%3C/svg%3E" />',
' </div>',
' <p class="onboarding-qr-caption">Scan this QR code with your phone</p>',
' </div>',
' <div class="onboarding-copy-panel">',
' <p class="onboarding-kicker onboarding-brand-title">Pulse Signage</p>',
' <h1>Quickly set up with your phone</h1>',
' <ol class="onboarding-instructions">',
' <li>Open the camera and scan the QR code.</li>',
' <li>Log in to Pulse Signage and choose a screen.</li>',
' </ol>',
' <div class="onboarding-pin-block">',
' <span class="onboarding-pin-label">Pairing Code</span>',
' <strong id="onboarding-pairing-code" class="onboarding-pin">' + Handlebars.escapeExpression(onboardingCode || 'Loading...') + '</strong>',
' </div>',
' </div>',
' </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>Pairing code</span>',
' <input name="pairingCode" type="text" inputmode="numeric" pattern="[0-9]{6}" maxlength="6" required autocomplete="one-time-code" placeholder="Enter the code shown on the kiosk" />',
' </label>',
' <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 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 bootstrapScript = getPlayerPageScript()({
SLUG_JSON: new Handlebars.SafeString(JSON.stringify(slug)),
INITIAL_DATA_JSON: new Handlebars.SafeString(safeJsonForScript(initialData || null)),
REGION_SCRIPTS: ''
});
const runtimeScriptTags = getPlayerRuntimeScripts().map(function (entry) {
return '<script src="/assets/player-script/' + encodeURIComponent(entry[0]) + '.js?v=' + encodeURIComponent(String(packageMetadata.version || 'development')) + '"></script>';
}).join('');
return renderPage(template, {
title: 'Screen ' + slug,
bodyClass: '',
body: '<div id="app"><div class="empty">Loading screen...</div></div>',
stylesheets: fontStylesheetHref ? [fontStylesheetHref] : [],
script: createPageFetchAuthScript(pageAuthToken, '/ws/screens/' + encodeURIComponent(slug || '')) + hlsScriptTag + serviceWorkerScript + runtimeScriptTags + bootstrapScript + '<script>' + getPlayerPageAnnouncementsScript()() + '</script>'
});
}
function renderPlayerOnboardingLandingPage(options) {
const pageAuthToken = createPageAuthBundle({ scope: 'onboarding' });
const fontStylesheetHref = getFontStylesheetHref(PLAYER_MEDIA_DIR);
return renderPage(getPlayerPageTemplate(), {
title: 'Onboard player',
bodyClass: 'onboarding-page',
body: renderOnboardingLandingBody(options),
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
};