Implement schedule WYSIWYG and UTC dates
This commit is contained in:
+14
-4
@@ -60,7 +60,7 @@ function createPlayerPlaylistService(options) {
|
||||
try {
|
||||
const [screenRows] = await pool.query('SELECT id, name, slug, playlist_id, created_at, modified_at, created_by, modified_by FROM d_screens WHERE slug = ?', [slug]);
|
||||
if (!screenRows.length) {
|
||||
return { screen: null, playlist: null, slides: [], rssFeeds: [], apiSources: [] };
|
||||
return { screen: null, playlist: null, slides: [], rssFeeds: [], apiSources: [], scheduleGroups: [] };
|
||||
}
|
||||
|
||||
const screen = screenRows[0];
|
||||
@@ -69,6 +69,9 @@ function createPlayerPlaylistService(options) {
|
||||
screen: screen,
|
||||
playlist: null,
|
||||
slides: [],
|
||||
rssFeeds: [],
|
||||
apiSources: [],
|
||||
scheduleGroups: [],
|
||||
revision: getPlaylistRevision(screen, null, [], [], [], [], [])
|
||||
};
|
||||
await writeSnapshot(slug, payloadWithoutPlaylist);
|
||||
@@ -184,8 +187,14 @@ function createPlayerPlaylistService(options) {
|
||||
});
|
||||
}
|
||||
|
||||
const revision = getPlaylistRevision(screen, playlist, slideRows, templateRows, regionRows, rssFeeds, apiSources);
|
||||
const payload = { screen: screen, playlist: playlist, slides: slides, rssFeeds: rssFeeds, apiSources: apiSources, revision: revision };
|
||||
let scheduleGroups = [];
|
||||
if (typeof common.fetchSchedulesData === 'function') {
|
||||
const scheduleData = await common.fetchSchedulesData(pool);
|
||||
scheduleGroups = Array.isArray(scheduleData && scheduleData.scheduleGroups) ? scheduleData.scheduleGroups : [];
|
||||
}
|
||||
|
||||
const revision = getPlaylistRevision(screen, playlist, slideRows, templateRows, regionRows, rssFeeds, apiSources, scheduleGroups);
|
||||
const payload = { screen: screen, playlist: playlist, slides: slides, rssFeeds: rssFeeds, apiSources: apiSources, scheduleGroups: scheduleGroups, revision: revision };
|
||||
await writeSnapshot(slug, payload);
|
||||
return payload;
|
||||
} catch (error) {
|
||||
@@ -202,7 +211,7 @@ function createPlayerPlaylistService(options) {
|
||||
hash.update('\0');
|
||||
}
|
||||
|
||||
function getPlaylistRevision(screen, playlist, slideRows, templateRows, regionRows, rssFeeds, apiSources) {
|
||||
function getPlaylistRevision(screen, playlist, slideRows, templateRows, regionRows, rssFeeds, apiSources, scheduleGroups) {
|
||||
const hash = crypto.createHash('sha1');
|
||||
|
||||
updatePlaylistRevisionHash(hash, screen && screen.id);
|
||||
@@ -259,6 +268,7 @@ function createPlayerPlaylistService(options) {
|
||||
|
||||
updatePlaylistRevisionHash(hash, JSON.stringify(rssFeeds || []));
|
||||
updatePlaylistRevisionHash(hash, JSON.stringify(apiSources || []));
|
||||
updatePlaylistRevisionHash(hash, JSON.stringify(scheduleGroups || []));
|
||||
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
@@ -463,29 +463,6 @@ body.screen-blackout #app {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.template-region table,
|
||||
.template-region th,
|
||||
.template-region td {
|
||||
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.template-region table {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.template-region th,
|
||||
.template-region td {
|
||||
padding: 0.35em 0.5em;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.template-region th {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.webpage-preloads {
|
||||
position: fixed;
|
||||
width: 1px;
|
||||
@@ -503,3 +480,20 @@ body.screen-blackout #app {
|
||||
border: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.template-region table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.template-region th,
|
||||
.template-region td {
|
||||
padding: 0.35em 0.5em;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.template-region th {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
+20
-1
@@ -1,6 +1,6 @@
|
||||
// Service worker cache strategy for player pages, assets, media, and playlists.
|
||||
|
||||
const CACHE_VERSION = 'v36';
|
||||
const CACHE_VERSION = 'v38';
|
||||
const PAGE_CACHE = `pulse-signage-player-pages-${CACHE_VERSION}`;
|
||||
const ASSET_CACHE = `pulse-signage-player-assets-${CACHE_VERSION}`;
|
||||
const MEDIA_CACHE = `pulse-signage-player-media-${CACHE_VERSION}`;
|
||||
@@ -16,6 +16,20 @@ function normalizeRequest(request) {
|
||||
});
|
||||
}
|
||||
|
||||
function shouldBypassCache(request) {
|
||||
if (!request) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (request.cache === 'reload' || request.cache === 'no-store') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cacheControl = String(request.headers.get('cache-control') || '').toLowerCase();
|
||||
const pragma = String(request.headers.get('pragma') || '').toLowerCase();
|
||||
return cacheControl.includes('no-cache') || cacheControl.includes('max-age=0') || pragma.includes('no-cache');
|
||||
}
|
||||
|
||||
async function cacheResponse(cacheName, request, response, cacheKeyRequest) {
|
||||
if (!response || !response.ok) {
|
||||
return;
|
||||
@@ -126,6 +140,11 @@ self.addEventListener('fetch', function (event) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldBypassCache(request)) {
|
||||
event.respondWith(networkOnly(request));
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith('/assets/')) {
|
||||
event.respondWith(cacheFirst(request, ASSET_CACHE));
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
// Schedule region rendering for live playback.
|
||||
|
||||
var registry = window.pulsePlayerRegionTypes;
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value === undefined || value === null ? '' : value)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function sanitizeRichTextAttributes(tagName, attrText) {
|
||||
var allowedAttributes = {
|
||||
a: ['href', 'title', 'target', 'rel', 'class', 'style'],
|
||||
blockquote: ['class', 'style'],
|
||||
col: ['class', 'style', 'span', 'width'],
|
||||
colgroup: ['class', 'style', 'span'],
|
||||
div: ['class', 'style'],
|
||||
figure: ['class', 'style'],
|
||||
figcaption: ['class', 'style'],
|
||||
h1: ['class', 'style'],
|
||||
h2: ['class', 'style'],
|
||||
h3: ['class', 'style'],
|
||||
h4: ['class', 'style'],
|
||||
h5: ['class', 'style'],
|
||||
h6: ['class', 'style'],
|
||||
li: ['class', 'style'],
|
||||
ol: ['class', 'style', 'start'],
|
||||
p: ['class', 'style'],
|
||||
pre: ['class', 'style'],
|
||||
span: ['class', 'style'],
|
||||
table: ['class', 'style'],
|
||||
tbody: ['class', 'style'],
|
||||
td: ['class', 'style', 'colspan', 'rowspan'],
|
||||
th: ['class', 'style', 'colspan', 'rowspan', 'scope'],
|
||||
thead: ['class', 'style'],
|
||||
tr: ['class', 'style'],
|
||||
ul: ['class', 'style']
|
||||
};
|
||||
var allowed = allowedAttributes[tagName] || [];
|
||||
if (!allowed.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var attrs = [];
|
||||
String(attrText || '').replace(/([a-zA-Z0-9:-]+)(?:\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>/=`]+)))?/g, function (_full, key, _valuePart, doubleQuoted, singleQuoted, bareValue) {
|
||||
var lowerKey = String(key || '').toLowerCase();
|
||||
if (allowed.indexOf(lowerKey) === -1) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var value = doubleQuoted !== undefined ? doubleQuoted : singleQuoted !== undefined ? singleQuoted : bareValue !== undefined ? bareValue : '';
|
||||
if (lowerKey === 'href' && /^(?:\s*javascript:|\s*data:)/i.test(String(value || ''))) {
|
||||
return '';
|
||||
}
|
||||
if (lowerKey === 'style' && /(?:expression\s*\(|javascript:|url\s*\()/i.test(String(value || ''))) {
|
||||
return '';
|
||||
}
|
||||
if (lowerKey === 'target') {
|
||||
var targetValue = String(value || '').trim();
|
||||
if (targetValue === '_blank') {
|
||||
attrs.push(' target="_blank"');
|
||||
if (attrs.indexOf(' rel="noreferrer noopener"') === -1) {
|
||||
attrs.push(' rel="noreferrer noopener"');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
attrs.push(' ' + lowerKey + '="' + escapeHtml(value) + '"');
|
||||
return '';
|
||||
});
|
||||
|
||||
return attrs.join('');
|
||||
}
|
||||
|
||||
function sanitizeRichText(html) {
|
||||
var output = String(html || '');
|
||||
output = output.replace(/<script[\s\S]*?<\/script>/gi, '');
|
||||
output = output.replace(/<style[\s\S]*?<\/style>/gi, '');
|
||||
return output.replace(/<[^>]+>/g, function (tag) {
|
||||
var match = tag.match(/^<\s*(\/?)\s*([a-z0-9]+)([\s\S]*?)(\/?)>$/i);
|
||||
if (!match) {
|
||||
return '';
|
||||
}
|
||||
var closing = Boolean(match[1]);
|
||||
var name = String(match[2] || '').toLowerCase();
|
||||
var allowed = ['a', 'b', 'blockquote', 'br', 'code', 'col', 'colgroup', 'div', 'em', 'figure', 'figcaption', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'li', 'ol', 'p', 'pre', 'span', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'th', 'thead', 'tr', 'u', 'ul'];
|
||||
if (allowed.indexOf(name) === -1) {
|
||||
return '';
|
||||
}
|
||||
if (closing) {
|
||||
return '</' + name + '>';
|
||||
}
|
||||
return '<' + name + sanitizeRichTextAttributes(name, String(match[3] || '')) + '>';
|
||||
});
|
||||
}
|
||||
|
||||
function substituteScheduleVariables(html, entry) {
|
||||
var source = String(html || '');
|
||||
return source.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) {
|
||||
if (!entry || typeof entry !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!window.placeholderUtils || typeof window.placeholderUtils.resolvePlaceholderExpression !== 'function' || typeof window.placeholderUtils.formatPlaceholderValue !== 'function') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return escapeHtml(window.placeholderUtils.formatPlaceholderValue(window.placeholderUtils.resolvePlaceholderExpression(entry, expression)));
|
||||
});
|
||||
}
|
||||
|
||||
function renderTemplate(template, context) {
|
||||
var source = String(template || '');
|
||||
if (!source) {
|
||||
return '';
|
||||
}
|
||||
return substituteScheduleVariables(source, context);
|
||||
}
|
||||
|
||||
function getScheduleGroups() {
|
||||
return Array.isArray(initialData && initialData.scheduleGroups) ? initialData.scheduleGroups : [];
|
||||
}
|
||||
|
||||
function getGroupById(groupId, groups) {
|
||||
var normalizedId = Number(groupId || 0);
|
||||
return (Array.isArray(groups) ? groups : getScheduleGroups()).find(function (group) {
|
||||
return Number(group.id) === normalizedId;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function getEntries(groupId, groups) {
|
||||
var group = getGroupById(groupId, groups);
|
||||
return group && Array.isArray(group.entries) ? group.entries : [];
|
||||
}
|
||||
|
||||
function toDate(value) {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
var date = value instanceof Date ? value : new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
function isUpcoming(entry, now) {
|
||||
var start = toDate(entry && entry.start_datetime);
|
||||
return Boolean(start && now < start);
|
||||
}
|
||||
|
||||
function isLive(entry, now) {
|
||||
var start = toDate(entry && entry.start_datetime);
|
||||
var end = toDate(entry && entry.end_datetime);
|
||||
return Boolean(start && end && now >= start && now < end);
|
||||
}
|
||||
|
||||
function getVisibleEntries(groupId, displayMode, maxItems, groups) {
|
||||
var now = new Date();
|
||||
var entries = getEntries(groupId, groups).slice().sort(function (left, right) {
|
||||
var leftStart = toDate(left && left.start_datetime);
|
||||
var rightStart = toDate(right && right.start_datetime);
|
||||
return (leftStart ? leftStart.getTime() : 0) - (rightStart ? rightStart.getTime() : 0) || Number(left.id || 0) - Number(right.id || 0);
|
||||
});
|
||||
var mode = String(displayMode || 'upcoming').trim().toLowerCase();
|
||||
|
||||
entries = entries.filter(function (entry) {
|
||||
if (mode === 'current') {
|
||||
return isLive(entry, now);
|
||||
}
|
||||
if (mode === 'both') {
|
||||
return isUpcoming(entry, now) || isLive(entry, now);
|
||||
}
|
||||
return isUpcoming(entry, now);
|
||||
});
|
||||
|
||||
return entries.slice(0, Math.max(1, Number(maxItems || 5)));
|
||||
}
|
||||
|
||||
function formatDateTime(value) {
|
||||
var date = toDate(value);
|
||||
if (!date) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
return new Intl.DateTimeFormat(undefined, {
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
}).format(date);
|
||||
} catch (_error) {
|
||||
return date.toLocaleString();
|
||||
}
|
||||
}
|
||||
|
||||
function getDefaultStyle() {
|
||||
return {
|
||||
font_family: 'Arial',
|
||||
font_size: 28,
|
||||
font_color: '#000000'
|
||||
};
|
||||
}
|
||||
|
||||
function getTextStyle(region, regionContent) {
|
||||
var current = regionContent && typeof regionContent === 'object' ? regionContent : {};
|
||||
var defaultStyle = getDefaultStyle();
|
||||
return {
|
||||
font_family: String(current.font_family || region.font_family || defaultStyle.font_family || 'Arial').trim() || 'Arial',
|
||||
font_size: Math.max(8, Number(current.font_size || region.font_size || defaultStyle.font_size || 28)),
|
||||
font_color: String(current.font_color || region.font_color || defaultStyle.font_color || '#000000').trim() || '#000000'
|
||||
};
|
||||
}
|
||||
|
||||
function renderRegion(region, regionContent) {
|
||||
var value = String(regionContent && (regionContent.value !== undefined ? regionContent.value : regionContent.text !== undefined ? regionContent.text : '') || '').trim();
|
||||
var style = getTextStyle(region, regionContent || {});
|
||||
var groups = getScheduleGroups();
|
||||
var groupId = regionContent && regionContent.schedule_group_id !== undefined ? regionContent.schedule_group_id : '';
|
||||
var displayMode = regionContent && regionContent.display_mode ? regionContent.display_mode : 'upcoming';
|
||||
var maxItems = regionContent && regionContent.max_items !== undefined ? regionContent.max_items : 5;
|
||||
var group = getGroupById(groupId, groups);
|
||||
var entries = getVisibleEntries(groupId, displayMode, maxItems, groups);
|
||||
if (!entries.length) {
|
||||
entries = getEntries(groupId, groups).slice(0, Math.max(1, Number(maxItems || 5)));
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return '<div class="template-region schedule" style="' + region.baseStyle + '"><div class="template-region-text-scale" style="width:100%;height:100%;overflow:hidden;' + (style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : '') + (style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size))) + 'px;' : '') + (style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : '') + '"></div></div>';
|
||||
}
|
||||
|
||||
return '<div class="template-region schedule" style="' + region.baseStyle + '"><div class="template-region-text-scale" style="width:100%;height:100%;overflow:hidden;' + (style.font_family ? 'font-family:' + escapeHtml(style.font_family) + ';' : '') + (style.font_size ? 'font-size:' + Math.max(1, Math.round(Number(style.font_size))) + 'px;' : '') + (style.font_color ? 'color:' + escapeHtml(style.font_color) + ';' : '') + '">' + entries.map(function (entry, index) {
|
||||
return '<div class="schedule-region-entry" data-schedule-entry-index="' + index + '">' + sanitizeRichText(substituteScheduleVariables(value, Object.assign({}, entry || {}, {
|
||||
start: entry && entry.start_datetime !== undefined ? entry.start_datetime : '',
|
||||
end: entry && entry.end_datetime !== undefined ? entry.end_datetime : '',
|
||||
group: group || {},
|
||||
entries: entries,
|
||||
index: index + 1
|
||||
}))) + '</div>';
|
||||
}).join('') + '</div></div>';
|
||||
}
|
||||
|
||||
registry.register('schedule', {
|
||||
renderRegion: renderRegion
|
||||
});
|
||||
@@ -27,7 +27,7 @@ function getPlayerServiceWorkerRegistrationScript() {
|
||||
'<script>',
|
||||
' if ("serviceWorker" in navigator) {',
|
||||
' window.addEventListener("load", function () {',
|
||||
' navigator.serviceWorker.register("/sw.js").catch(function () {',
|
||||
' navigator.serviceWorker.register("/sw.js?v=38").catch(function () {',
|
||||
' return null;',
|
||||
' });',
|
||||
' });',
|
||||
|
||||
@@ -259,6 +259,11 @@ function registerPlayerRoutes(app, options) {
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof common.fetchSchedulesData === 'function') {
|
||||
const scheduleData = await common.fetchSchedulesData(pool);
|
||||
data.scheduleGroups = Array.isArray(scheduleData && scheduleData.scheduleGroups) ? scheduleData.scheduleGroups : [];
|
||||
}
|
||||
|
||||
res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
||||
res.send(common.renderPlayerPage('slide-thumbnail-preview-' + slide.id, data));
|
||||
} catch (error) {
|
||||
|
||||
@@ -15,6 +15,7 @@ function buildThumbnailPreviewData(slide) {
|
||||
slides: [slide],
|
||||
rssFeeds: [],
|
||||
apiSources: [],
|
||||
scheduleGroups: [],
|
||||
revision: String(slide.modified_at || slide.id || Date.now())
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user