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

This commit is contained in:
lzstealth committed 2026-10-12 00:56:09 +01:00
1 parent c3299abaac
commit ddcc665f81
47 files changed
+1209 -66

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
.weather-icon-upload-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: .5rem;
}
.weather-icon-upload-section + .weather-icon-upload-section {
margin-top: 1rem;
}
.weather-icon-upload-row-label {
color: var(--bs-body-color);
font-size: .875rem;
font-weight: 600;
margin-bottom: .5rem;
}
.weather-icon-upload-row-label span {
color: var(--bs-secondary-color);
font-weight: 400;
}
.weather-icon-upload-tile {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: .25rem;
width: 100%;
height: 112px;
padding: .5rem;
color: var(--bs-body-color);
background: var(--bs-tertiary-bg);
border: 1px dashed var(--bs-border-color);
border-radius: 4px;
}
.weather-icon-upload-tile:hover,
.weather-icon-upload-tile:focus-visible {
border-color: var(--bs-primary);
background: var(--bs-secondary-bg);
}
.weather-icon-upload-tile:focus-visible {
outline: 2px solid var(--bs-primary);
outline-offset: 2px;
}
.weather-icon-upload-preview {
display: flex;
align-items: center;
justify-content: center;
width: 56px;
font-size: 1.5rem;
color: var(--bs-secondary-color);
}
.weather-icon-upload-label {
font-size: .75rem;
line-height: 1.25;
text-align: center;
overflow-wrap: anywhere;
}
.weather-icon-upload.is-missing .weather-icon-upload-tile {
border-color: var(--bs-warning);
background: color-mix(in srgb, var(--bs-warning) 10%, var(--bs-tertiary-bg));
}
.weather-icon-upload.is-invalid .weather-icon-upload-tile {
border-color: var(--bs-danger);
background: color-mix(in srgb, var(--bs-danger) 10%, var(--bs-tertiary-bg));
}
.weather-icon-upload-status {
color: var(--bs-warning-text-emphasis);
font-size: .75rem;
font-weight: 600;
}
.weather-icon-upload.is-invalid .weather-icon-upload-status {
color: var(--bs-danger-text-emphasis);
}
+10 -2
View File
@@ -119,8 +119,16 @@
? 'display:block;width:100%;height:100%;object-fit:contain;'
: 'display:block;width:auto;height:auto;' + (imageConfig.width ? 'max-width:' + imageConfig.width + 'px;' : '') + (imageConfig.height ? 'max-height:' + imageConfig.height + 'px;' : '');
var image = '<img src="' + escapeHtml(imageSource) + '" alt="" style="' + imageStyle + '" />';
if (options && options.preview && imageConfig.width && imageConfig.height) {
return '<span style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:' + imageConfig.width + 'px;height:' + imageConfig.height + 'px;border:1px dashed rgba(120,120,120,.75);">' + image + '</span>';
var hasPreviewBounds = options && options.preview && imageConfig.width && imageConfig.height;
if (hasPreviewBounds || imageConfig.verticalAlign) {
var wrapperStyle = 'display:inline-flex;align-items:center;justify-content:center;';
if (hasPreviewBounds) {
wrapperStyle += 'box-sizing:border-box;width:' + imageConfig.width + 'px;height:' + imageConfig.height + 'px;border:1px dashed rgba(120,120,120,.75);';
}
if (imageConfig.verticalAlign) {
wrapperStyle += 'vertical-align:' + imageConfig.verticalAlign + ';';
}
return '<span style="' + wrapperStyle + '">' + image + '</span>';
}
return image;
}
+10 -2
View File
@@ -138,8 +138,16 @@
? 'display:block;width:100%;height:100%;object-fit:contain;'
: 'display:block;width:auto;height:auto;' + (imageConfig.width ? 'max-width:' + imageConfig.width + 'px;' : '') + (imageConfig.height ? 'max-height:' + imageConfig.height + 'px;' : '');
var image = '<img src="' + escapeHtml(imageSource) + '" alt="" style="' + imageStyle + '" />';
if (options && options.preview && imageConfig.width && imageConfig.height) {
return '<span style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:' + imageConfig.width + 'px;height:' + imageConfig.height + 'px;border:1px dashed rgba(120,120,120,.75);">' + image + '</span>';
var hasPreviewBounds = options && options.preview && imageConfig.width && imageConfig.height;
if (hasPreviewBounds || imageConfig.verticalAlign) {
var wrapperStyle = 'display:inline-flex;align-items:center;justify-content:center;';
if (hasPreviewBounds) {
wrapperStyle += 'box-sizing:border-box;width:' + imageConfig.width + 'px;height:' + imageConfig.height + 'px;border:1px dashed rgba(120,120,120,.75);';
}
if (imageConfig.verticalAlign) {
wrapperStyle += 'vertical-align:' + imageConfig.verticalAlign + ';';
}
return '<span style="' + wrapperStyle + '">' + image + '</span>';
}
return image;
}
+34 -9
View File
@@ -109,7 +109,7 @@
return data;
}
function substituteVariables(html, value) {
function substituteVariables(html, value, iconSetId) {
return String(html || '').replace(/\{\{\s*([^{}]+?)\s*\}\}/g, function (_match, expression) {
if (!value || typeof value !== 'object' || typeof placeholderUtils.resolvePlaceholderExpression !== 'function' || typeof placeholderUtils.formatPlaceholderValue !== 'function') return '';
var rawExpression = String(expression).trim();
@@ -117,16 +117,20 @@
if (typeof placeholderUtils.isProgressPlaceholderExpression === 'function' && placeholderUtils.isProgressPlaceholderExpression(rawExpression)) {
return placeholderUtils.renderProgressPlaceholder(value, rawExpression);
}
var iconMatch = normalizedExpression.match(/^(?:current\.weather_code|daily\.weather_code\.\d+|hourly\.weather_code\.\d+)\.icon(?:\((\d+)(?:\s*,\s*(\d+))?\))?$/);
var iconMatch = normalizedExpression.match(/^(?:current\.weather_code|daily\.weather_code\.\d+|hourly\.weather_code\.\d+)\.icon(?:\((\d+)(?:\s*,\s*(\d+))?(?:\s*,\s*(top|middle|bottom))?\))?$/);
if (iconMatch) {
var codeExpression = normalizedExpression.replace(/\.icon(?:\(.*\))?$/, '');
var sizeExpression = rawExpression.match(/\.icon\((\d+)(?:\s*,\s*(\d+))?\)$/);
var sizeExpression = rawExpression.match(/\.icon\((\d+)(?:\s*,\s*(\d+))?(?:\s*,\s*(top|middle|bottom))?\)$/);
var weatherCode = placeholderUtils.resolvePlaceholderExpression(value, codeExpression, { timeZone: value.timezone });
var width = sizeExpression ? Math.max(1, Math.min(1000, Number(sizeExpression[1]))) : 0;
var height = sizeExpression && sizeExpression[2] ? Math.max(1, Math.min(1000, Number(sizeExpression[2]))) : width;
var style = width ? ' style="display:inline-block;vertical-align:middle;font-size:' + width + 'px;line-height:' + height + 'px;width:' + width + 'px;height:' + height + 'px;"' : '';
var alignment = sizeExpression && sizeExpression[3] || 'middle';
var source = window.pulseWeatherIcons && window.pulseWeatherIcons.imageForCode(window.pulseWeatherIconSets, iconSetId, weatherCode, window.pulseWeatherIcons.isNightForWeather(value));
if (source) return window.pulseWeatherIcons.renderImage(source, width, height, alignment);
var style = width ? ' style="display:block;font-size:' + width + 'px;line-height:1;width:' + width + 'px;height:' + height + 'px;"' : '';
var icon = '<i class="bi ' + iconForCode(weatherCode) + '"' + style + ' aria-hidden="true"></i>';
return width ? '<span style="display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;width:' + width + 'px;height:' + height + 'px;border:1px dashed rgba(120,120,120,.75);">' + icon + '</span>' : icon;
var verticalAlign = alignment === 'top' || alignment === 'bottom' ? alignment : 'middle';
return width ? '<span style="display:inline-flex;vertical-align:' + verticalAlign + ';align-items:center;justify-content:center;box-sizing:border-box;line-height:0;width:' + width + 'px;height:' + height + 'px;border:1px dashed rgba(120,120,120,.75);">' + icon + '</span>' : icon;
}
return escapeHtml(placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression(value, normalizeWeatherExpression(expression), { timeZone: value.timezone })));
});
@@ -138,7 +142,7 @@
'<div class="offcanvas offcanvas-end fw-normal" tabindex="-1" id="' + offcanvasId + '" aria-labelledby="' + offcanvasId + '-label">' +
'<div class="offcanvas-header"><h2 class="offcanvas-title fs-5" id="' + offcanvasId + '-label">Weather placeholder transforms</h2><button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button></div>' +
'<div class="offcanvas-body"><p class="small text-body-secondary">Placeholders read values from the selected weather snapshot. Nested fields use dots.</p>' +
'<h3 class="h6 mt-3">Weather icons</h3><p class="small text-body-secondary">Use the icon property to insert a Bootstrap weather icon. Add width and height in pixels when sizing is needed.</p><ul class="small"><li><code>{{current.icon}}</code></li><li><code>{{current.icon(48,48)}}</code></li><li><code>{{daily.0.icon(32,24)}}</code></li></ul>' +
'<h3 class="h6 mt-3">Weather icons</h3><p class="small text-body-secondary">Use the icon property to insert a weather icon. Add width, height, and optional top, middle, or bottom alignment when sizing is needed.</p><ul class="small"><li><code>{{current.icon}}</code></li><li><code>{{current.icon(48,48)}}</code></li><li><code>{{daily.0.icon(32,96,top)}}</code></li></ul>' +
(window.placeholderInfo ? window.placeholderInfo.renderTextTransforms() : '') +
(window.placeholderInfo ? window.placeholderInfo.renderMathTransforms() : '') +
(window.placeholderInfo ? window.placeholderInfo.renderDateFormatTokens() : '') +
@@ -154,9 +158,10 @@
var snapshot = getSnapshot(location);
if (!location || !snapshot) return '';
var weatherData = withWeatherUnits(snapshot, location);
weatherData.iconSetId = content && content.weather_icon_set_id;
if (value) {
var fontSize = Math.max(8, Number(content && content.font_size || region && (region.font_size || region.fontSize) || 32) || 32);
return '<div class="template-region weather" style="width:100%;height:100%;overflow:hidden;font-family:Arial;font-size:' + fontSize + 'px;line-height:1.5;color:#000000;"><div class="template-region-text-scale" style="width:100%;height:100%;overflow:hidden;">' + (utils.sanitizePreviewHtml ? utils.sanitizePreviewHtml(substituteVariables(value, weatherData)) : substituteVariables(value, weatherData)) + '</div></div>';
return '<div class="template-region weather" style="width:100%;height:100%;overflow:hidden;font-family:Arial;font-size:' + fontSize + 'px;line-height:1.5;color:#000000;"><div class="template-region-text-scale" style="width:100%;height:100%;overflow:hidden;">' + (utils.sanitizePreviewHtml ? utils.sanitizePreviewHtml(substituteVariables(value, weatherData, weatherData.iconSetId)) : substituteVariables(value, weatherData, weatherData.iconSetId)) + '</div></div>';
}
var current = snapshot.current || {};
var daily = snapshot.daily || {};
@@ -198,14 +203,34 @@
return { region: context.region, current: context.current || {}, weatherLocations: context.weatherLocations || [] };
}
function renderEditorCardWithIcons(context) {
var html = renderEditorCard(context);
var id = context.region.id;
var selected = context.current && context.current.weather_icon_set_id || '';
var sets = Array.isArray(window.pulseWeatherIconSets) ? window.pulseWeatherIconSets : [];
var options = sets.filter(function (set) { return set.enabled !== false; }).map(function (set) {
return '<option value="' + escapeHtml(set.id) + '"' + (selected === set.id ? ' selected' : '') + '>' + escapeHtml(set.name) + '</option>';
}).join('');
var selectedSet = sets.find(function (set) { return set.id === selected; });
if (selectedSet && selectedSet.enabled === false) {
options += '<option value="' + escapeHtml(selected) + '" selected>' + escapeHtml(selectedSet.name) + '</option>';
} else if (selected && !selectedSet) {
options += '<option value="' + escapeHtml(selected) + '" selected>Unavailable set</option>';
}
var control = '<div class="col-12 col-lg-3"><label class="form-label" for="region_weather_icon_set_id_' + escapeHtml(id) + '">Icon set</label><select class="form-select" id="region_weather_icon_set_id_' + escapeHtml(id) + '" name="region_weather_icon_set_id_' + escapeHtml(id) + '"><option value="">Built-in icons</option>' + options + '</select></div>';
html = html.replace('class="col-12 col-md-8"', 'class="col-12 col-lg-6"').replace('class="col-12 col-md-4"', 'class="col-12 col-lg-3"');
return html.replace('</select></div></div><input type="hidden" name="region_text', '</select></div>' + control + '</div><input type="hidden" name="region_text');
}
function buildPreviewRenderContext(region, card, existingContent, locations) {
var current = existingContent[region.region_key] || {};
var input = card && card.querySelector('select[name="region_weather_location_id_' + region.id + '"]');
var modeInput = card && card.querySelector('select[name="region_weather_forecast_mode_' + region.id + '"]');
var iconInput = card && card.querySelector('select[name="region_weather_icon_set_id_' + region.id + '"]');
var textInput = card && card.querySelector('input[name="region_text_' + region.id + '"]');
var textArea = card && card.querySelector('textarea.editor-source');
return { value: textInput ? textInput.value : textArea ? textArea.value : current.value || '', weather_location_id: input ? input.value : current.weather_location_id, forecast_mode: modeInput ? modeInput.value : current.forecast_mode || 'daily', weatherLocations: locations || [] };
return { value: textInput ? textInput.value : textArea ? textArea.value : current.value || '', weather_location_id: input ? input.value : current.weather_location_id, weather_icon_set_id: iconInput ? iconInput.value : current.weather_icon_set_id || '', forecast_mode: modeInput ? modeInput.value : current.forecast_mode || 'daily', weatherLocations: locations || [] };
}
registry.register('weather', { label: 'Weather', getDefaultStyle: getDefaultStyle, getDefaultRegionSize: function () { return { width: 720, height: 260 }; }, renderPreview: renderPreview, renderEditorCard: renderEditorCard, buildEditorCardContext: buildEditorCardContext, buildPreviewRenderContext: buildPreviewRenderContext });
registry.register('weather', { label: 'Weather', getDefaultStyle: getDefaultStyle, getDefaultRegionSize: function () { return { width: 720, height: 260 }; }, renderPreview: renderPreview, renderEditorCard: renderEditorCardWithIcons, buildEditorCardContext: buildEditorCardContext, buildPreviewRenderContext: buildPreviewRenderContext });
}());
@@ -0,0 +1,59 @@
(function () {
function validateSetForm(form) {
var missing = Array.prototype.slice.call(form.querySelectorAll('[data-weather-icon-upload][data-weather-icon-required]')).filter(function (tile) {
var input = tile.querySelector('[data-weather-icon-file]');
var preview = tile.querySelector('[data-weather-icon-preview]');
return !(input.files && input.files.length) && !preview.getAttribute('src');
});
form.querySelectorAll('[data-weather-icon-upload]').forEach(function (tile) {
tile.classList.toggle('is-invalid', missing.includes(tile));
});
if (!missing.length) return true;
var labels = missing.map(function (tile) {
var label = tile.querySelector('[data-weather-icon-picker]');
return String(label && label.getAttribute('aria-label') || '').replace(/^Upload\s+|\s+icon$/g, '');
}).filter(Boolean);
if (typeof window.showToast === 'function') {
window.showToast('Add images for: ' + labels.join(', ') + '.', 'warning');
}
missing[0].querySelector('[data-weather-icon-picker]').focus();
return false;
}
document.querySelectorAll('[data-weather-icon-upload]').forEach(function (tile) {
var picker = tile.querySelector('[data-weather-icon-picker]');
var input = tile.querySelector('[data-weather-icon-file]');
var preview = tile.querySelector('[data-weather-icon-preview]');
var empty = tile.querySelector('[data-weather-icon-empty]');
var status = tile.querySelector('[data-weather-icon-status]');
var originalSource = preview.getAttribute('src') || '';
var previewUrl = '';
function updatePreview() {
if (previewUrl) URL.revokeObjectURL(previewUrl);
var file = input.files && input.files[0];
previewUrl = file ? URL.createObjectURL(file) : '';
var source = previewUrl || originalSource;
if (source) preview.src = source;
else preview.removeAttribute('src');
preview.hidden = !source;
empty.hidden = Boolean(source);
tile.classList.toggle('is-missing', !source && tile.hasAttribute('data-weather-icon-required'));
tile.classList.remove('is-invalid');
if (status) status.hidden = Boolean(source);
}
picker.addEventListener('click', function () { input.click(); });
input.addEventListener('change', updatePreview);
input.form.addEventListener('reset', function () { setTimeout(updatePreview, 0); });
window.addEventListener('pagehide', function () {
if (previewUrl) URL.revokeObjectURL(previewUrl);
});
updatePreview();
});
document.querySelectorAll('[data-weather-icon-set-form]').forEach(function (form) {
form.addEventListener('submit', function (event) {
if (!validateSetForm(form)) event.preventDefault();
});
});
}());
+2 -2
View File
@@ -58,8 +58,8 @@
function renderImageTransform() {
return '<h3 class="fs-6 mt-4 fw-normal">Image transform</h3>' +
'<p class="small">Use <code>image(width, height)</code> to render an image URL inside a proportional bounding box.</p>' +
'<ul class="small"><li>The preview shows the width and height boundary.</li><li>The player has no boundary.</li><li>The image keeps its original aspect ratio.</li><li>Both dimensions are required for a bounding box.</li></ul>';
'<p class="small">Use <code>image(width, height, alignment)</code> to render an image URL inside a proportional bounding box. Alignment can be <code>top</code>, <code>middle</code>, or <code>bottom</code> to position it relative to surrounding text.</p>' +
'<ul class="small"><li>The preview shows the width and height boundary.</li><li>The player has no boundary.</li><li>The image keeps its original aspect ratio.</li><li>Both dimensions are required for a bounding box.</li><li>Alignment is optional; without it, existing image alignment is unchanged.</li></ul>';
}
root.placeholderInfo = {
@@ -386,9 +386,11 @@
var args = imageTransform && Array.isArray(imageTransform.args) ? imageTransform.args : [];
var width = Number(args[0]);
var height = Number(args[1]);
var verticalAlign = String(args[2] || '').trim().toLowerCase();
return {
width: Number.isFinite(width) && width > 0 ? Math.round(width) : 0,
height: Number.isFinite(height) && height > 0 ? Math.round(height) : 0
height: Number.isFinite(height) && height > 0 ? Math.round(height) : 0,
verticalAlign: ['top', 'middle', 'bottom'].includes(verticalAlign) ? verticalAlign : ''
};
}
+34
View File
@@ -0,0 +1,34 @@
(function (root) {
function conditionForCode(code) {
if (code === null || code === undefined || code === '') return '';
var value = Number(code);
if (value === 0) return 'clear';
if (value === 1 || value === 2) return 'partly-cloudy';
if (value === 3) return 'overcast';
if ([45, 48].includes(value)) return 'fog';
if ([51, 53, 55, 56, 57].includes(value)) return 'drizzle';
if ([61, 63, 65, 66, 67].includes(value)) return 'rain';
if ([71, 73, 75, 77, 85, 86].includes(value)) return 'snow';
if ([80, 81, 82].includes(value)) return 'showers';
if ([95, 96, 99].includes(value)) return 'thunderstorm';
return '';
}
function imageForCode(sets, id, code, isNight) {
var set = (Array.isArray(sets) ? sets : []).find(function (entry) { return entry.id === id; });
var condition = conditionForCode(code);
var source = set && (isNight && set.nightIcons && set.nightIcons[condition] || set.icons && set.icons[condition]);
return typeof source === 'string' && /^\/media\/(?:assets\/)?weather-icons\/[a-f0-9-]+\/[a-z-]+-[a-f0-9]+\.(?:png|svg)$/.test(source) ? source : '';
}
function isNightForWeather(data) {
return Boolean(data && data.current && Number(data.current.is_day) === 0);
}
function renderImage(source, width, height, alignment) {
var iconWidth = width ? width + 'px' : '1em';
var iconHeight = height ? height + 'px' : '1em';
var verticalAlign = alignment === 'top' || alignment === 'bottom' ? alignment : 'middle';
return '<span style="display:inline-flex;vertical-align:' + verticalAlign + ';align-items:center;justify-content:center;box-sizing:border-box;line-height:0;width:' + iconWidth + ';height:' + iconHeight + ';"><img src="' + source + '" alt="" style="display:block;width:100%;height:100%;object-fit:contain;" /></span>';
}
var api = { conditionForCode: conditionForCode, imageForCode: imageForCode, isNightForWeather: isNightForWeather, renderImage: renderImage };
if (typeof module === 'object' && module.exports) module.exports = api;
else root.pulseWeatherIcons = api;
}(typeof window === 'object' ? window : globalThis));
+1
View File
@@ -23,6 +23,7 @@ import { createSlideFormPreviewHelpers } from '/assets/js/slides/slide-form-prev
var apiSources = Array.isArray(slideEditorData.apiSources) ? slideEditorData.apiSources : [];
var timetableGroups = Array.isArray(slideEditorData.timetableGroups) ? slideEditorData.timetableGroups : [];
var weatherLocations = Array.isArray(slideEditorData.weatherLocations) ? slideEditorData.weatherLocations : [];
window.pulseWeatherIconSets = Array.isArray(slideEditorData.weatherIconSets) ? slideEditorData.weatherIconSets : [];
var fontStylesheetHref = String(slideEditorData.fontStylesheetHref || '').trim();
var existingTemplateId = slideEditorData.existingTemplateId !== undefined ? slideEditorData.existingTemplateId : null;
var existingContent = slideEditorData.existingContent || {};