Release v2.15.4
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m50s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 34s

This commit is contained in:
lzstealth committed 2026-10-17 16:01:00 +01:00
1 parent 64aeb1b47e
commit 05c9341b08
25 files changed
+595 -31

No files matched your search

+93 -14
View File
@@ -98,15 +98,40 @@
return Boolean(start && now < start);
}
function toLocalInput(value) {
var date = toDate(value);
if (!date) {
return '';
}
function pad(part) { return String(part).padStart(2, '0'); }
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
}
function getDateRangeConfig(regionId, card, current) {
var config = {};
['date_filter', 'date_from', 'date_to'].forEach(function (field) {
var input = card && card.querySelector ? card.querySelector('[name="region_' + field + '_' + regionId + '"]') : null;
config[field] = input ? input.value : current[field] || (field === 'date_filter' ? 'all' : '');
});
return config;
}
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) {
function getVisibleEntries(groupId, displayMode, maxItems, groups, config) {
var now = new Date();
var entries = getEntries(groupId, groups).slice().sort(function (left, right) {
var range = config || {};
var from = toDate(range.date_filter === 'between' || range.date_filter === 'after' ? range.date_from : '');
var to = toDate(range.date_filter === 'between' || range.date_filter === 'before' ? range.date_to : '');
var candidates = getEntries(groupId, groups).filter(function (entry) {
var start = toDate(entry && entry.start_datetime);
return (!from && !to) || Boolean(start && (!from || start >= from) && (!to || start <= to));
});
var entries = candidates.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);
@@ -124,7 +149,7 @@
});
if (!entries.length && mode === 'upcoming') {
entries = getEntries(groupId, groups).filter(function (entry) {
entries = candidates.filter(function (entry) {
return isLive(entry, now);
});
}
@@ -167,6 +192,9 @@
display_mode: String(current.display_mode || current.timetable_display_mode || 'upcoming').trim().toLowerCase() || 'upcoming',
value: String(current.text !== undefined ? current.text : current.value !== undefined ? current.value : ''),
max_items: Math.max(1, Number(current.max_items || current.timetable_max_items || 5)),
date_filter: ['between', 'before', 'after'].includes(current.date_filter) ? current.date_filter : 'all',
date_from: current.date_from || '',
date_to: current.date_to || '',
font_family: current.font_family || region.font_family || getDefaultStyle().font_family,
font_size: current.font_size || region.font_size || getDefaultStyle().font_size,
font_color: current.font_color || region.font_color || getDefaultStyle().font_color
@@ -230,8 +258,9 @@
var displayMode = timetableDisplayModeInput ? timetableDisplayModeInput.value : String(current.display_mode || current.timetable_display_mode || 'upcoming').trim().toLowerCase() || 'upcoming';
var maxItems = Math.max(1, Number(timetableMaxItemsInput ? timetableMaxItemsInput.value : current.max_items || current.timetable_max_items || 5));
var group = getGroupById(groupId, timetableGroups);
var entries = getVisibleEntries(groupId, displayMode, maxItems, timetableGroups);
if (!entries.length) {
var range = getDateRangeConfig(regionId, card, current);
var entries = getVisibleEntries(groupId, displayMode, maxItems, timetableGroups, range);
if (!entries.length && range.date_filter === 'all') {
entries = getEntries(groupId, timetableGroups).slice(0, maxItems);
}
@@ -267,9 +296,10 @@
var currentGroupId = current.timetable_group_id === undefined || current.timetable_group_id === null || current.timetable_group_id === '' ? '' : Number(current.timetable_group_id);
var currentDisplayMode = String(current.display_mode || current.timetable_display_mode || 'upcoming').trim().toLowerCase() || 'upcoming';
var currentMaxItems = Math.max(1, Number(current.max_items || current.timetable_max_items || 5));
var dateFilter = ['between', 'before', 'after'].includes(current.date_filter) ? current.date_filter : 'all';
var currentGroup = getGroupById(currentGroupId, timetableGroups);
var currentEntries = getVisibleEntries(currentGroupId, currentDisplayMode, currentMaxItems, timetableGroups);
if (!currentEntries.length) {
var currentEntries = getVisibleEntries(currentGroupId, currentDisplayMode, currentMaxItems, timetableGroups, current);
if (!currentEntries.length && dateFilter === 'all') {
currentEntries = getEntries(currentGroupId, timetableGroups).slice(0, currentMaxItems);
}
var groupOptions = timetableGroups.map(function (group) {
@@ -295,7 +325,7 @@
'<input type="hidden" name="region_text_' + region.id + '" value="' + escapeHtml(currentValue) + '" />' +
'</div>' +
'<div class="row g-3">' +
'<div class="col-12 col-lg-6">' +
'<div class="col-12 col-lg-4">' +
'<label class="form-label" for="region_timetable_group_id_' + region.id + '">Timetable group</label>' +
'<select id="region_timetable_group_id_' + region.id + '" name="region_timetable_group_id_' + region.id + '" class="form-select">' +
'<option value="">Select a group</option>' +
@@ -310,12 +340,34 @@
'<option value="both"' + (currentDisplayMode === 'both' ? ' selected' : '') + '>Both</option>' +
'</select>' +
'</div>' +
'<div class="col-6 col-lg-3">' +
'<div class="col-6 col-lg-2">' +
'<label class="form-label" for="region_timetable_max_items_' + region.id + '">Max items</label>' +
'<input id="region_timetable_max_items_' + region.id + '" type="number" min="1" step="1" name="region_timetable_max_items_' + region.id + '" class="form-control" value="' + escapeHtml(currentMaxItems || 5) + '" />' +
'</div>' +
'<div class="col-12 col-lg-3">' +
'<label class="form-label" for="region_date_filter_' + region.id + '">Date filter</label>' +
'<select id="region_date_filter_' + region.id + '" name="region_date_filter_' + region.id + '" class="form-select">' +
'<option value="all"' + (dateFilter === 'all' ? ' selected' : '') + '>All</option>' +
'<option value="between"' + (dateFilter === 'between' ? ' selected' : '') + '>Between Dates</option>' +
'<option value="before"' + (dateFilter === 'before' ? ' selected' : '') + '>Before Date</option>' +
'<option value="after"' + (dateFilter === 'after' ? ' selected' : '') + '>After Date</option>' +
'</select>' +
'</div>' +
'</div>' +
'<div data-timetable-date-range' + (dateFilter === 'all' ? ' hidden' : '') + '>' +
'<div class="row g-3">' +
['from', 'to'].map(function (boundary) {
var field = 'date_' + boundary;
var enabled = dateFilter === 'between' || dateFilter === (boundary === 'from' ? 'after' : 'before');
var label = boundary === 'from' ? 'Starts on or after (local time)' : 'Starts on or before (local time)';
return '<div class="col-12 col-md-6" data-timetable-boundary' + (!enabled ? ' hidden' : '') + '>' +
'<label class="form-label" for="region_' + field + '_' + region.id + '">' + label + '</label>' +
'<input id="region_' + field + '_' + region.id + '" type="datetime-local" step="1" class="form-control" data-timetable-date="' + field + '" value="' + escapeHtml(toLocalInput(current[field])) + '"' + (!enabled ? ' disabled' : '') + ' />' +
'<input type="hidden" name="region_' + field + '_' + region.id + '" value="' + escapeHtml(current[field] || '') + '" />' +
'</div>';
}).join('') +
'</div>' +
'</div>' +
'<div class="text-body-secondary small">Use the selected group and display mode to choose which timetable entry fields are available.</div>' +
'<div class="api-region-placeholder-section schedule-placeholder-section" data-schedule-placeholder-chips>' +
'<div class="api-region-placeholder-title api-region-placeholder-title-help d-flex align-items-center gap-2">Available placeholders ' + renderPlaceholderInfo(region.id) + '</div>' +
'<div class="d-flex flex-wrap gap-2">' + renderSchedulePlaceholderChips(currentGroup, currentEntries) + '</div>' +
@@ -350,24 +402,50 @@
panel.innerHTML = renderSchedulePlaceholderChips(current.group, current.entries);
}
function updateDateRange(card) {
var regionId = card.getAttribute('data-region-id');
var config = getDateRangeConfig(regionId, card, {});
var panel = card.querySelector('[data-timetable-date-range]');
if (!panel) {
return;
}
panel.hidden = config.date_filter === 'all';
panel.querySelectorAll('[data-timetable-date]').forEach(function (input) {
var field = input.getAttribute('data-timetable-date');
var enabled = config.date_filter === 'between' || config.date_filter === (field === 'date_from' ? 'after' : 'before');
input.closest('[data-timetable-boundary]').hidden = !enabled;
input.disabled = !enabled;
var date = toDate(input.value);
card.querySelector('[name="region_' + input.getAttribute('data-timetable-date') + '_' + regionId + '"]').value = date ? date.toISOString() : '';
});
var fromInput = panel.querySelector('[data-timetable-date="date_from"]');
var toInput = panel.querySelector('[data-timetable-date="date_to"]');
var from = toDate(fromInput.value);
var to = toDate(toInput.value);
toInput.setCustomValidity(config.date_filter === 'between' && from && to && from > to ? 'The end of the date range must be on or after its start.' : '');
}
if (!window.__scheduleRegionPlaceholderRefreshInstalled) {
window.__scheduleRegionPlaceholderRefreshInstalled = true;
document.addEventListener('change', function (event) {
function handleTimetableInput(event) {
var target = event.target;
if (!target || !target.closest) {
return;
}
if (!target.closest('select[name^="region_timetable_group_id_"], select[name^="region_timetable_display_mode_"], input[name^="region_timetable_max_items_"]')) {
if (!target.closest('select[name^="region_timetable_group_id_"], select[name^="region_timetable_display_mode_"], input[name^="region_timetable_max_items_"], select[name^="region_date_filter_"], [data-timetable-date]')) {
return;
}
var card = target.closest('[data-region-id]');
if (card) {
updateDateRange(card);
updateSchedulePlaceholderPanel(card);
}
}, true);
}
document.addEventListener('input', handleTimetableInput, true);
document.addEventListener('change', handleTimetableInput, true);
}
function buildEditorCardContext(context) {
@@ -410,6 +488,7 @@
tz_long: timezoneValues.tz_long,
timeZone: timezoneValues.tz_long,
max_items: timetableMaxItemsInput ? timetableMaxItemsInput.value || current.max_items : current.max_items,
...getDateRangeConfig(region.id, card, current),
existingContent: existingContent || {},
timetableGroups: Array.isArray(timetableGroups) ? timetableGroups : []
};
@@ -423,7 +502,7 @@
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);
var entries = getVisibleEntries(groupId, displayMode, maxItems, groups, regionContent);
if (!value) {
return '<div class="slide-preview-region timetable" 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>';