Release v2.5.4
This commit is contained in:
@@ -2,6 +2,17 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## 2.5.4 - 2026-08-03
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The API source bearer token field now uses a password input with a right-side toggle to show or hide the token.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- QR code regions now render at the correct size in the slide editor preview, and the QR chip header now matches the other region cards.
|
||||||
|
- The dashboard onboarding link now uses the player public base URL stored in the database instead of a `/screen/...` URL.
|
||||||
|
|
||||||
## 2.5.3 - 2026-08-03
|
## 2.5.3 - 2026-08-03
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
Generated
+9
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.5.1",
|
"version": "2.5.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.5.1",
|
"version": "2.5.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sparticuz/chromium": "^137.0.0",
|
"@sparticuz/chromium": "^137.0.0",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"mysql2": "^3.14.3",
|
"mysql2": "^3.14.3",
|
||||||
"puppeteer-core": "^24.16.0",
|
"puppeteer-core": "^24.16.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
|
"qrcode-generator": "^2.0.4",
|
||||||
"sharp": "^0.35.3",
|
"sharp": "^0.35.3",
|
||||||
"ws": "^8.21.0"
|
"ws": "^8.21.0"
|
||||||
},
|
},
|
||||||
@@ -2708,6 +2709,12 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode-generator": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode-generator/-/qrcode-generator-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-mZSiP6RnbHl4xL2Ap5HfkjLnmxfKcPWpWe/c+5XxCuetEenqmNFf1FH/ftXPCtFG5/TDobjsjz6sSNL0Sr8Z9g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.15.3",
|
"version": "6.15.3",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-signage",
|
"name": "pulse-signage",
|
||||||
"version": "2.5.3",
|
"version": "2.5.4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "Pulse Signage application with MySQL and media storage",
|
"description": "Pulse Signage application with MySQL and media storage",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
"mysql2": "^3.14.3",
|
"mysql2": "^3.14.3",
|
||||||
"puppeteer-core": "^24.16.0",
|
"puppeteer-core": "^24.16.0",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
|
"qrcode-generator": "^2.0.4",
|
||||||
"sharp": "^0.35.3",
|
"sharp": "^0.35.3",
|
||||||
"ws": "^8.21.0"
|
"ws": "^8.21.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-1
@@ -22,9 +22,10 @@ async function fetchAdminData(pool) {
|
|||||||
ORDER BY s.id DESC
|
ORDER BY s.id DESC
|
||||||
`);
|
`);
|
||||||
const [screens] = await pool.query(`
|
const [screens] = await pool.query(`
|
||||||
SELECT s.id, s.name, s.slug, s.playlist_id, s.created_at, s.modified_at, s.created_by, s.modified_by, p.name AS playlist_name
|
SELECT s.id, s.name, s.slug, s.playlist_id, s.created_at, s.modified_at, s.created_by, s.modified_by, p.name AS playlist_name, pl.public_base_url
|
||||||
FROM d_screens s
|
FROM d_screens s
|
||||||
LEFT JOIN c_playlists p ON p.id = s.playlist_id
|
LEFT JOIN c_playlists p ON p.id = s.playlist_id
|
||||||
|
LEFT JOIN d_players pl ON pl.device_id = s.player_id
|
||||||
ORDER BY s.id DESC
|
ORDER BY s.id DESC
|
||||||
`);
|
`);
|
||||||
const [playlistSlides] = await pool.query(`
|
const [playlistSlides] = await pool.query(`
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
const QRCode = require('qrcode');
|
||||||
|
|
||||||
|
async function createQrCodeSvg(value) {
|
||||||
|
const text = String(value === undefined || value === null ? '' : value).trim();
|
||||||
|
if (!text) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return QRCode.toString(text, {
|
||||||
|
type: 'svg',
|
||||||
|
margin: 1,
|
||||||
|
errorCorrectionLevel: 'M'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildQrCodeContent(value) {
|
||||||
|
const text = String(value === undefined || value === null ? '' : value).trim();
|
||||||
|
const content = {
|
||||||
|
type: 'qr-code',
|
||||||
|
value: text
|
||||||
|
};
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
const svg = await createQrCodeSvg(text);
|
||||||
|
if (svg) {
|
||||||
|
content.qr_svg = svg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createQrCodeSvg,
|
||||||
|
buildQrCodeContent
|
||||||
|
};
|
||||||
+23
-2
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const { fetchTemplateById } = require('./templates');
|
const { fetchTemplateById } = require('./templates');
|
||||||
const { parseJsonSafe } = require('./utils');
|
const { parseJsonSafe } = require('./utils');
|
||||||
|
const { buildQrCodeContent } = require('./qr-code');
|
||||||
|
|
||||||
const ALLOWED_RICH_TEXT_TAGS = ['b', 'strong', 'i', 'em', 'u', 'br', 'p', 'div', 'ul', 'ol', 'li'];
|
const ALLOWED_RICH_TEXT_TAGS = ['b', 'strong', 'i', 'em', 'u', 'br', 'p', 'div', 'ul', 'ol', 'li'];
|
||||||
const DEFAULT_FONT_SIZE = 32;
|
const DEFAULT_FONT_SIZE = 32;
|
||||||
@@ -142,6 +143,14 @@ function buildTemplateContent(template, body, filesByField, existingContent) {
|
|||||||
type: 'webpage',
|
type: 'webpage',
|
||||||
value: submitted === undefined ? current : String(submitted || '').trim()
|
value: submitted === undefined ? current : String(submitted || '').trim()
|
||||||
};
|
};
|
||||||
|
} else if (region.region_type === 'qr-code') {
|
||||||
|
const submitted = body[`region_qr_code_${region.id}`];
|
||||||
|
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key] : {};
|
||||||
|
const nextValue = submitted === undefined ? String(current.value !== undefined ? current.value : current.qr_code || '').trim() : String(submitted || '').trim();
|
||||||
|
content[region.region_key] = {
|
||||||
|
type: 'qr-code',
|
||||||
|
value: nextValue
|
||||||
|
};
|
||||||
} else if (region.region_type === 'rtmp') {
|
} else if (region.region_type === 'rtmp') {
|
||||||
const submitted = body[`region_rtmp_${region.id}`];
|
const submitted = body[`region_rtmp_${region.id}`];
|
||||||
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key] : {};
|
const current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key] : {};
|
||||||
@@ -202,7 +211,7 @@ function buildTemplateContent(template, body, filesByField, existingContent) {
|
|||||||
font_size: style.font_size,
|
font_size: style.font_size,
|
||||||
font_color: style.font_color
|
font_color: style.font_color
|
||||||
};
|
};
|
||||||
} else if (!['text', 'image', 'video', 'webpage', 'html', 'rtmp', 'rss', 'api'].includes(String(region.region_type || '').trim())) {
|
} else if (!['text', 'image', 'video', 'webpage', 'qr-code', 'html', 'rtmp', 'rss', 'api'].includes(String(region.region_type || '').trim())) {
|
||||||
const current = existingContent && existingContent[region.region_key] && typeof existingContent[region.region_key] === 'object' ? existingContent[region.region_key] : {};
|
const current = existingContent && existingContent[region.region_key] && typeof existingContent[region.region_key] === 'object' ? existingContent[region.region_key] : {};
|
||||||
const suffix = '_' + region.id;
|
const suffix = '_' + region.id;
|
||||||
const generic = {};
|
const generic = {};
|
||||||
@@ -273,10 +282,22 @@ async function buildSlidePayload(pool, req, existingSlide) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (template) {
|
if (template) {
|
||||||
|
const content = buildTemplateContent(template, req.body, filesByField, existingContent);
|
||||||
|
await Promise.all(Object.keys(content).map(async function (regionKey) {
|
||||||
|
const region = template.regions.find(function (item) {
|
||||||
|
return String(item.region_key || '').trim() === regionKey;
|
||||||
|
});
|
||||||
|
if (!region || region.region_type !== 'qr-code') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
content[regionKey] = await buildQrCodeContent(content[regionKey].value);
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
templateId: template.id,
|
templateId: template.id,
|
||||||
contentJson: JSON.stringify(buildTemplateContent(template, req.body, filesByField, existingContent))
|
contentJson: JSON.stringify(content)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -443,6 +443,14 @@ body.screen-blackout #app {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.template-region.qr-code img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.template-region.html iframe {
|
.template-region.html iframe {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// QR code region rendering for embedded URL-to-image output.
|
||||||
|
|
||||||
|
var registry = window.pulsePlayerRegionTypes;
|
||||||
|
|
||||||
|
function renderQrCodeRegion(region, regionContent) {
|
||||||
|
var svg = String(regionContent && regionContent.qr_svg || '').trim();
|
||||||
|
if (!svg) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<div class="template-region qr-code" style="' + region.baseStyle + '"><img class="template-region-qr-code-image" src="data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg) + '" alt="QR code" /></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.register('qr-code', {
|
||||||
|
renderRegion: renderQrCodeRegion
|
||||||
|
});
|
||||||
@@ -114,6 +114,13 @@ function buildRegionInnerHtml(region, regionContent, baseUrl) {
|
|||||||
: '';
|
: '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (regionType === 'qr-code') {
|
||||||
|
const src = String(regionContent.qr_svg || rawValue || regionContent.qr_code || '').trim();
|
||||||
|
return src
|
||||||
|
? '<img src="data:image/svg+xml;charset=utf-8,' + encodeURIComponent(src) + '" alt="' + escapeHtml(region.label || region.region_key || 'qr code') + '" />'
|
||||||
|
: '';
|
||||||
|
}
|
||||||
|
|
||||||
if (regionType === 'html') {
|
if (regionType === 'html') {
|
||||||
const html = String(rawValue || '').trim();
|
const html = String(rawValue || '').trim();
|
||||||
return html
|
return html
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
module.exports = function registerMiddleware(app, deps) {
|
module.exports = function registerMiddleware(app, deps) {
|
||||||
const pool = deps.pool;
|
const pool = deps.pool;
|
||||||
@@ -11,10 +12,12 @@ module.exports = function registerMiddleware(app, deps) {
|
|||||||
const uploadsDir = deps.UPLOADS_DIR;
|
const uploadsDir = deps.UPLOADS_DIR;
|
||||||
const thumbnailsDir = deps.THUMBNAILS_DIR;
|
const thumbnailsDir = deps.THUMBNAILS_DIR;
|
||||||
const assetDir = deps.ASSET_DIR;
|
const assetDir = deps.ASSET_DIR;
|
||||||
|
const qrcodeGeneratorDir = path.join(__dirname, '..', '..', 'node_modules', 'qrcode-generator', 'dist');
|
||||||
|
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use('/assets', express.static(assetDir));
|
app.use('/assets', express.static(assetDir));
|
||||||
|
app.use('/assets/vendor/qrcode-generator', express.static(qrcodeGeneratorDir));
|
||||||
app.use('/media', express.static(mediaDir));
|
app.use('/media', express.static(mediaDir));
|
||||||
fs.mkdirSync(uploadsDir, { recursive: true });
|
fs.mkdirSync(uploadsDir, { recursive: true });
|
||||||
fs.mkdirSync(thumbnailsDir, { recursive: true });
|
fs.mkdirSync(thumbnailsDir, { recursive: true });
|
||||||
|
|||||||
@@ -9,6 +9,29 @@
|
|||||||
var methodSelect = form.querySelector('[data-api-source-auth-method]');
|
var methodSelect = form.querySelector('[data-api-source-auth-method]');
|
||||||
var authDetailsSection = form.querySelector('[data-api-source-auth-details-section]');
|
var authDetailsSection = form.querySelector('[data-api-source-auth-details-section]');
|
||||||
var panels = Array.prototype.slice.call(form.querySelectorAll('[data-api-source-auth-panel]'));
|
var panels = Array.prototype.slice.call(form.querySelectorAll('[data-api-source-auth-panel]'));
|
||||||
|
var bearerTokenInput = form.querySelector('[data-api-source-bearer-token-input]');
|
||||||
|
var bearerTokenToggle = form.querySelector('[data-api-source-bearer-token-toggle]');
|
||||||
|
|
||||||
|
function updateBearerTokenToggle() {
|
||||||
|
if (!bearerTokenInput || !bearerTokenToggle) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var isVisible = bearerTokenInput.type === 'text';
|
||||||
|
bearerTokenToggle.setAttribute('aria-pressed', String(isVisible));
|
||||||
|
bearerTokenToggle.setAttribute('aria-label', isVisible ? 'Hide bearer token' : 'Show bearer token');
|
||||||
|
bearerTokenToggle.innerHTML = '<i class="bi ' + (isVisible ? 'bi-eye-slash' : 'bi-eye') + '" aria-hidden="true"></i>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBearerTokenVisibility() {
|
||||||
|
if (!bearerTokenInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bearerTokenInput.type = bearerTokenInput.type === 'password' ? 'text' : 'password';
|
||||||
|
updateBearerTokenToggle();
|
||||||
|
bearerTokenInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
function updatePanels() {
|
function updatePanels() {
|
||||||
var method = String(methodSelect && methodSelect.value || 'none').trim();
|
var method = String(methodSelect && methodSelect.value || 'none').trim();
|
||||||
@@ -28,5 +51,10 @@
|
|||||||
methodSelect.addEventListener('change', updatePanels);
|
methodSelect.addEventListener('change', updatePanels);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (bearerTokenToggle && bearerTokenInput) {
|
||||||
|
bearerTokenToggle.addEventListener('click', toggleBearerTokenVisibility);
|
||||||
|
updateBearerTokenToggle();
|
||||||
|
}
|
||||||
|
|
||||||
updatePanels();
|
updatePanels();
|
||||||
}());
|
}());
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
// QR code region helpers for editor previews and defaults.
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
var registry = window.pulseRegionTypes;
|
||||||
|
var utils = window.pulseRegionUtils || {};
|
||||||
|
|
||||||
|
function escapeHtml(value) {
|
||||||
|
return utils.escapeHtml ? utils.escapeHtml(value) : String(value === undefined || value === null ? '' : value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildQrCodeUrl(value) {
|
||||||
|
var text = String(value && typeof value === 'object' ? (value.value !== undefined ? value.value : value.qr_code) : value || '').trim();
|
||||||
|
var svg = String(value && typeof value === 'object' && value.qr_svg ? value.qr_svg : '').trim();
|
||||||
|
if (svg) {
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!text || typeof window.qrcode !== 'function') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
var code = window.qrcode(0, 'M');
|
||||||
|
code.addData(text);
|
||||||
|
code.make();
|
||||||
|
svg = code.createSvgTag(4, 1);
|
||||||
|
return svg || '';
|
||||||
|
} catch (_error) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeQrSvg(svg) {
|
||||||
|
var markup = String(svg || '').trim();
|
||||||
|
if (!markup || markup.charAt(0) !== '<') {
|
||||||
|
return markup;
|
||||||
|
}
|
||||||
|
|
||||||
|
return markup.replace(/^<svg\b([^>]*)>/i, function (_match, attrText) {
|
||||||
|
var attrs = String(attrText || '');
|
||||||
|
if (!/\bwidth\s*=\s*/i.test(attrs)) {
|
||||||
|
attrs += ' width="95%"';
|
||||||
|
}
|
||||||
|
if (!/\bheight\s*=\s*/i.test(attrs)) {
|
||||||
|
attrs += ' height="95%"';
|
||||||
|
}
|
||||||
|
if (!/\bpreserveAspectRatio\s*=\s*/i.test(attrs)) {
|
||||||
|
attrs += ' preserveAspectRatio="xMidYMid meet"';
|
||||||
|
}
|
||||||
|
if (!/\bstyle\s*=\s*/i.test(attrs)) {
|
||||||
|
attrs += ' style="display:block;width:95%;height:95%;"';
|
||||||
|
}
|
||||||
|
return '<svg' + attrs + '>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPreview(region, value) {
|
||||||
|
var src = buildQrCodeUrl(value);
|
||||||
|
if (!src) {
|
||||||
|
return '<div class="slide-preview-placeholder">QR Code</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (String(src || '').trim().charAt(0) === '<') {
|
||||||
|
return '<div class="slide-preview-qr-code-frame" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#fff;overflow:hidden;">' + normalizeQrSvg(src) + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '<img class="slide-preview-qr-code" src="' + escapeHtml(src) + '" alt="QR code preview" style="width:100%;height:100%;object-fit:contain;display:block;background:#fff;" />';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEditorCard(context) {
|
||||||
|
var region = context.region;
|
||||||
|
var current = String(context.current || '');
|
||||||
|
return '' +
|
||||||
|
'<div class="card card-outline card-secondary admin-form-card template-field-card mb-3" data-region-id="' + region.id + '">' +
|
||||||
|
'<div class="card-header template-field-head">' +
|
||||||
|
'<strong>' + escapeHtml(region.label) + '</strong>' +
|
||||||
|
'<div class="template-field-actions">' +
|
||||||
|
'<span class="chip">QR Code</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'<div class="card-body p-3 d-grid gap-2">' +
|
||||||
|
'<input type="url" name="region_qr_code_' + region.id + '" class="form-control" value="' + escapeHtml(current) + '" placeholder="https://example.com" />' +
|
||||||
|
'</label>' +
|
||||||
|
'<div class="muted slide-image-file">The QR code is generated from the URL above.</div>' +
|
||||||
|
'</div>' +
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildEditorCardContext(context, existingContent) {
|
||||||
|
var existing = existingContent && context && context.region && existingContent[context.region.region_key] ? existingContent[context.region.region_key] : null;
|
||||||
|
return {
|
||||||
|
region: context.region,
|
||||||
|
current: String(context.current || (existing && (existing.value !== undefined ? existing.value : existing.qr_code)) || ''),
|
||||||
|
qr_svg: existing && existing.qr_svg ? existing.qr_svg : ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPreviewRenderContext(region, card, existingContent) {
|
||||||
|
var current = existingContent && existingContent[region.region_key] ? existingContent[region.region_key] : {};
|
||||||
|
var input = card && card.querySelector ? card.querySelector('input[type="url"][name="region_qr_code_' + region.id + '"]') : null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: input ? input.value : (current && current.value !== undefined ? current.value : (current && current.qr_code !== undefined ? current.qr_code : '')),
|
||||||
|
existingContent: existingContent || {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.register('qr-code', {
|
||||||
|
label: 'QR Code',
|
||||||
|
getDefaultRegionSize: function () {
|
||||||
|
return { width: 240, height: 240 };
|
||||||
|
},
|
||||||
|
renderPreview: renderPreview,
|
||||||
|
renderEditorCard: renderEditorCard,
|
||||||
|
buildEditorCardContext: buildEditorCardContext,
|
||||||
|
buildPreviewRenderContext: buildPreviewRenderContext
|
||||||
|
});
|
||||||
|
}());
|
||||||
@@ -592,6 +592,8 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
|
|||||||
? ' slide-preview-video-region'
|
? ' slide-preview-video-region'
|
||||||
: region.region_type === 'webpage'
|
: region.region_type === 'webpage'
|
||||||
? ' slide-preview-webpage-region'
|
? ' slide-preview-webpage-region'
|
||||||
|
: region.region_type === 'qr-code'
|
||||||
|
? ' slide-preview-qr-code-region'
|
||||||
: region.region_type === 'rtmp'
|
: region.region_type === 'rtmp'
|
||||||
? ' slide-preview-rtmp-region'
|
? ' slide-preview-rtmp-region'
|
||||||
: region.region_type === 'rss'
|
: region.region_type === 'rss'
|
||||||
|
|||||||
@@ -2,7 +2,17 @@
|
|||||||
|
|
||||||
const { renderView } = require('../../../view');
|
const { renderView } = require('../../../view');
|
||||||
|
|
||||||
|
function normalizeBaseUrl(value) {
|
||||||
|
return String(value || '').trim().replace(/\/$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = function renderDashboardPage(data, message, currentUser) {
|
module.exports = function renderDashboardPage(data, message, currentUser) {
|
||||||
|
const primaryPlayerUrl = Array.isArray(data.screens)
|
||||||
|
? String((data.screens.find(function (screen) {
|
||||||
|
return screen && String(screen.public_base_url || '').trim();
|
||||||
|
}) || {}).public_base_url || '').trim()
|
||||||
|
: '';
|
||||||
|
|
||||||
return renderView('dashboard/index', {
|
return renderView('dashboard/index', {
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
active: 'dashboard',
|
active: 'dashboard',
|
||||||
@@ -13,6 +23,7 @@ module.exports = function renderDashboardPage(data, message, currentUser) {
|
|||||||
clients: data.clients || [],
|
clients: data.clients || [],
|
||||||
slides: data.slides || [],
|
slides: data.slides || [],
|
||||||
connectedClientsCount: Number(data.connectedClientsCount || 0),
|
connectedClientsCount: Number(data.connectedClientsCount || 0),
|
||||||
|
primaryPlayerUrl: normalizeBaseUrl(primaryPlayerUrl) || null,
|
||||||
scripts: ['js/dashboard/dashboard-page.js']
|
scripts: ['js/dashboard/dashboard-page.js']
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ function buildSlideFormViewModel(data, slide, message, currentUser, isEdit) {
|
|||||||
existingContent: viewSlide && viewSlide.content ? viewSlide.content : {}
|
existingContent: viewSlide && viewSlide.content ? viewSlide.content : {}
|
||||||
},
|
},
|
||||||
assetVersion: assetVersion,
|
assetVersion: assetVersion,
|
||||||
slideEditorScripts: getRegionEditorScripts(assetVersion),
|
slideEditorScripts: ['vendor/qrcode-generator/qrcode.js?v=' + assetVersion].concat(getRegionEditorScripts(assetVersion)),
|
||||||
currentUser: currentUser || null
|
currentUser: currentUser || null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ function buildTemplateFormViewModel(template, message, canvasSizes, currentUser,
|
|||||||
deleteUrl: isEdit && current.id ? '/templates/' + current.id + '/delete' : '',
|
deleteUrl: isEdit && current.id ? '/templates/' + current.id + '/delete' : '',
|
||||||
canvasSizes: canvasSizes || [],
|
canvasSizes: canvasSizes || [],
|
||||||
animationPresets: animationPresets,
|
animationPresets: animationPresets,
|
||||||
scripts: ['js/lib/modal.js?v=' + assetVersion, 'js/templates/animation-presets.js?v=' + assetVersion].concat(getRegionEditorScripts(assetVersion), ['js/templates/template-designer-utils.js?v=' + assetVersion, 'js/templates/template-designer.js?v=' + assetVersion])
|
scripts: ['js/lib/modal.js?v=' + assetVersion, 'js/templates/animation-presets.js?v=' + assetVersion, 'vendor/qrcode-generator/qrcode.js?v=' + assetVersion].concat(getRegionEditorScripts(assetVersion), ['js/templates/template-designer-utils.js?v=' + assetVersion, 'js/templates/template-designer.js?v=' + assetVersion])
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,12 @@
|
|||||||
<div class="row g-3 mb-3" data-api-source-auth-panel="bearer" hidden>
|
<div class="row g-3 mb-3" data-api-source-auth-panel="bearer" hidden>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label for="api-source-auth-bearer-token" class="form-label">Bearer token</label>
|
<label for="api-source-auth-bearer-token" class="form-label">Bearer token</label>
|
||||||
<input id="api-source-auth-bearer-token" name="auth_bearer_token" type="password" class="form-control" value="{{apiSource.authBearerToken}}" autocomplete="off" />
|
<div class="input-group">
|
||||||
|
<input id="api-source-auth-bearer-token" name="auth_bearer_token" type="password" class="form-control" value="{{apiSource.authBearerToken}}" autocomplete="off" spellcheck="false" autocapitalize="off" autocorrect="off" data-api-source-bearer-token-input />
|
||||||
|
<button type="button" class="btn btn-outline-secondary" aria-label="Show bearer token" aria-pressed="false" data-api-source-bearer-token-toggle>
|
||||||
|
<i class="bi bi-eye" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row g-3 mb-3" data-api-source-auth-panel="api_key_header" hidden>
|
<div class="row g-3 mb-3" data-api-source-auth-panel="api_key_header" hidden>
|
||||||
|
|||||||
@@ -11,6 +11,13 @@
|
|||||||
<span class="dashboard-hero-kicker">Live overview</span>
|
<span class="dashboard-hero-kicker">Live overview</span>
|
||||||
<h3 class="dashboard-hero-title">Keep the control surface focused on live state and actions.</h3>
|
<h3 class="dashboard-hero-title">Keep the control surface focused on live state and actions.</h3>
|
||||||
<p class="dashboard-hero-copy">Use the cards below for the current totals, then open the screen snapshot when you want a quick read on playlist assignment and live connections.</p>
|
<p class="dashboard-hero-copy">Use the cards below for the current totals, then open the screen snapshot when you want a quick read on playlist assignment and live connections.</p>
|
||||||
|
{{#if primaryPlayerUrl}}
|
||||||
|
<div class="alert alert-info mb-0 mt-3 py-2">
|
||||||
|
<strong>Begin onboarding:</strong>
|
||||||
|
open <a href="{{primaryPlayerUrl}}" target="_blank" rel="noreferrer">{{primaryPlayerUrl}}</a>
|
||||||
|
on the public screen to get started.
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-hero-stats">
|
<div class="dashboard-hero-stats">
|
||||||
{{#if (hasPermission currentUser "playlists.read")}}
|
{{#if (hasPermission currentUser "playlists.read")}}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@
|
|||||||
<textarea id="slide-editor-data" hidden>{{json slideEditorData}}</textarea>
|
<textarea id="slide-editor-data" hidden>{{json slideEditorData}}</textarea>
|
||||||
<script src="/assets/vendor/cropperjs/cropper.min.js"></script>
|
<script src="/assets/vendor/cropperjs/cropper.min.js"></script>
|
||||||
<script src="/assets/vendor/tinymce/tinymce.min.js"></script>
|
<script src="/assets/vendor/tinymce/tinymce.min.js"></script>
|
||||||
|
<script src="/assets/vendor/qrcode-generator/qrcode.js?v={{appVersion}}"></script>
|
||||||
{{#each slideEditorScripts}}
|
{{#each slideEditorScripts}}
|
||||||
<script type="module" src="/assets/{{this}}?v={{../appVersion}}"></script>
|
<script type="module" src="/assets/{{this}}?v={{../appVersion}}"></script>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|||||||
@@ -125,6 +125,8 @@
|
|||||||
<form id="delete-template-form" method="post" action="/templates/{{template.id}}/delete" data-confirm-message="Delete this template?"></form>
|
<form id="delete-template-form" method="post" action="/templates/{{template.id}}/delete" data-confirm-message="Delete this template?"></form>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
<script src="/assets/vendor/qrcode-generator/qrcode.js?v={{appVersion}}"></script>
|
||||||
|
|
||||||
{{#> modal-shell modalId="region-add-modal" modalLabelId="region-add-modal-label" modalDialogClass="modal-dialog-centered"}}
|
{{#> modal-shell modalId="region-add-modal" modalLabelId="region-add-modal-label" modalDialogClass="modal-dialog-centered"}}
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h2 class="modal-title fs-5" id="region-add-modal-label">Add region</h2>
|
<h2 class="modal-title fs-5" id="region-add-modal-label">Add region</h2>
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const { createQrCodeSvg, buildQrCodeContent } = require('../src/data/qr-code');
|
||||||
|
const { buildSlidePayload } = require('../src/data/slides');
|
||||||
|
|
||||||
|
test('createQrCodeSvg renders a qr svg for non-empty values', async () => {
|
||||||
|
const svg = await createQrCodeSvg('https://example.com');
|
||||||
|
|
||||||
|
assert.equal(typeof svg, 'string');
|
||||||
|
assert.match(svg, /^<svg[\s>]/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildQrCodeContent stores qr svg content for qr-code regions', async () => {
|
||||||
|
const content = await buildQrCodeContent('https://example.com');
|
||||||
|
|
||||||
|
assert.equal(content.type, 'qr-code');
|
||||||
|
assert.equal(content.value, 'https://example.com');
|
||||||
|
assert.equal(typeof content.qr_svg, 'string');
|
||||||
|
assert.match(content.qr_svg, /^<svg[\s>]/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildSlidePayload stores qr-code slide content from the submitted url', async () => {
|
||||||
|
const pool = {
|
||||||
|
async query(sql) {
|
||||||
|
if (sql.includes('FROM c_templates st')) {
|
||||||
|
return [[{ id: 7, name: 'Template 7', canvas_size_id: 1, canvas_size_width: 1920, canvas_size_height: 1080 }]];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sql.includes('FROM c_template_regions')) {
|
||||||
|
return [[{ id: 22, template_id: 7, region_key: 'qrRegion', region_type: 'qr-code', label: 'QR' }]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [[]];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const payload = await buildSlidePayload(pool, {
|
||||||
|
body: {
|
||||||
|
title: 'QR Slide',
|
||||||
|
template_id: '7',
|
||||||
|
region_qr_code_22: 'https://example.com'
|
||||||
|
},
|
||||||
|
files: []
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
const content = JSON.parse(payload.contentJson);
|
||||||
|
assert.equal(content.qrRegion.type, 'qr-code');
|
||||||
|
assert.equal(content.qrRegion.value, 'https://example.com');
|
||||||
|
assert.equal(typeof content.qrRegion.qr_svg, 'string');
|
||||||
|
assert.match(content.qrRegion.qr_svg, /^<svg[\s>]/);
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
|
||||||
|
const renderDashboardPage = require('../src/web/routes/signage/dashboard');
|
||||||
|
|
||||||
|
test('dashboard onboarding link uses the player base url', () => {
|
||||||
|
const html = renderDashboardPage(
|
||||||
|
{
|
||||||
|
screens: [
|
||||||
|
{
|
||||||
|
public_base_url: 'http://player.local/'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
playlists: [],
|
||||||
|
clients: [],
|
||||||
|
slides: [],
|
||||||
|
connectedClientsCount: 0
|
||||||
|
},
|
||||||
|
'',
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.match(html, /href="http:\/\/player\.local"/);
|
||||||
|
assert.doesNotMatch(html, /http:\/\/player\.local\//);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user