diff --git a/CHANGELOG.md b/CHANGELOG.md index f859e3c..dbebbd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. +## 2.19.3 - 2026-10-25 + +### Added + +- Slide-list thumbnails open an aspect-ratio-matched modal with a full-resolution preview of the saved slide. + +### Changed + +- Thumbnail previews use the shared region renderers so time/date and data placeholders match the editor preview. +- Preview framing follows the application theme and preserves the modal's rounded corners. + +### Fixed + +- Removed editor-only image and weather-icon placeholder bounds from popup previews while keeping them visible in the editor. +- Protected saved-slide popup previews with the existing slide-read authorization check. + ## 2.19.2 - 2026-10-24 ### Fixed diff --git a/build/package.player.json b/build/package.player.json index 7cc682c..e043a25 100644 --- a/build/package.player.json +++ b/build/package.player.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-player", - "version": "2.19.2", + "version": "2.19.3", "private": false, "description": "Pulse Signage player application bundle", "engines": { diff --git a/build/package.web.json b/build/package.web.json index b5afd5f..c252c47 100644 --- a/build/package.web.json +++ b/build/package.web.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-web", - "version": "2.19.2", + "version": "2.19.3", "private": false, "description": "Pulse Signage web and bridge application bundle", "engines": { diff --git a/package-lock.json b/package-lock.json index 22e2bbf..09efe9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pulse-signage", - "version": "2.19.2", + "version": "2.19.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pulse-signage", - "version": "2.19.2", + "version": "2.19.3", "dependencies": { "animate.css": "^4.1.1", "bootstrap-icons": "1.13.1", diff --git a/package.json b/package.json index 3885b81..5df16d2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.19.2", + "version": "2.19.3", "private": false, "description": "Pulse Signage application with MySQL and media storage", "engines": { diff --git a/src/web/lib/media/slide-thumbnails.js b/src/web/lib/media/slide-thumbnails.js index dcffae7..c6e0291 100644 --- a/src/web/lib/media/slide-thumbnails.js +++ b/src/web/lib/media/slide-thumbnails.js @@ -15,6 +15,7 @@ const { } = require('#src/player/render-helpers'); const { createRequestAuthHeaders, createPageAuthToken } = require('#src/request-auth'); const { convertWeatherSnapshot } = require('#src/data/weather-units'); +const { applyLayout } = require('#src/web/public/js/shared/region-layout'); const placeholderUtils = require('#src/web/public/js/shared/placeholder-utils'); const weatherData = require('#src/web/public/js/shared/weather-data'); @@ -335,6 +336,10 @@ function buildThumbnailPreviewMarkup(slide, baseUrl, options) { function buildThumbnailPreviewPayload(slide, options) { const template = slide && slide.template ? slide.template : null; const canvasSize = getThumbnailCanvasSize(slide); + const regions = (Array.isArray(template && template.regions) ? template.regions : []).map(function (region) { + const content = getRegionContent(slide, region); + return Object.assign({}, applyLayout(region, content, canvasSize), { content: content }); + }); return { thumbnailPreview: true, canvasWidth: canvasSize.width, @@ -342,6 +347,13 @@ function buildThumbnailPreviewPayload(slide, options) { backgroundColor: template && template.background_color ? String(template.background_color) : '#111111', backgroundGradient: template && template.background_gradient ? String(template.background_gradient) : '', backgroundImagePath: template && template.background_image_path ? String(template.background_image_path) : '', + regions: regions, + existingContent: slide && slide.content && typeof slide.content === 'object' ? slide.content : {}, + apiSources: Array.isArray(options && options.apiSources) ? options.apiSources : [], + rssFeeds: Array.isArray(options && options.rssFeeds) ? options.rssFeeds : [], + weatherLocations: Array.isArray(options && options.weatherLocations) ? options.weatherLocations : [], + timetableGroups: Array.isArray(options && options.timetableGroups) ? options.timetableGroups : [], + weatherIconSets: Array.isArray(options && options.weatherIconSets) ? options.weatherIconSets : [], fontStylesheetHref: String(options && options.fontStylesheetHref || '').trim(), html: buildThumbnailPreviewMarkup(slide, options && options.baseUrl, options) }; diff --git a/src/web/public/css/theme-custom.css b/src/web/public/css/theme-custom.css index b52ab24..e8d7fac 100644 --- a/src/web/public/css/theme-custom.css +++ b/src/web/public/css/theme-custom.css @@ -3146,8 +3146,8 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection { width: 100%; height: 100%; overflow: hidden; - background: #111; - color: #fff; + background: var(--bs-body-bg); + color: var(--bs-body-color); font-family: Arial, sans-serif; } @@ -3156,7 +3156,7 @@ html[data-bs-theme='dark'] .template-field-card .tox .tox-collection { width: 100%; height: 100%; overflow: hidden; - background: #111; + background: var(--bs-body-bg); } .slide-preview-popup-canvas { @@ -4577,6 +4577,36 @@ table.table > :not(caption) > * > * { justify-content: center; } +.slide-list-thumbnail-button { + padding: 0; + border: 0; + color: inherit; + cursor: pointer; +} + +.slide-list-thumbnail-button:focus-visible { + outline: 2px solid var(--bs-primary); + outline-offset: 2px; +} + +.slide-list-preview-frame { + display: block; + width: 100%; + height: auto; + aspect-ratio: var(--slide-preview-aspect-ratio, 16 / 9); + border: 0; + background: var(--bs-body-bg); +} + +#slide-preview-modal .modal-content { + overflow: hidden; +} + +#slide-preview-modal .modal-dialog { + width: var(--slide-preview-modal-width, min(90vw, 1140px)); + max-width: 90vw; +} + .card .table td[data-label="Thumbnail"] { width: 1%; padding-inline: 0.5rem; diff --git a/src/web/public/js/regions/type/api.js b/src/web/public/js/regions/type/api.js index cdfe735..d253987 100644 --- a/src/web/public/js/regions/type/api.js +++ b/src/web/public/js/regions/type/api.js @@ -106,7 +106,7 @@ var hasPreviewBounds = options && options.preview && imageConfig.width && imageConfig.height; if (hasPreviewBounds || imageConfig.verticalAlign) { var wrapperStyle = 'display:inline-flex;align-items:center;justify-content:center;'; - var wrapperClass = hasPreviewBounds ? ' class="api-image-placeholder-bounds"' : ''; + var wrapperClass = hasPreviewBounds ? ' class="api-image-placeholder-bounds slide-preview-image-placeholder-bounds"' : ''; if (hasPreviewBounds) { wrapperStyle += 'box-sizing:border-box;width:' + imageConfig.width + 'px;height:' + imageConfig.height + 'px;border:1px dashed rgba(120,120,120,.75);'; } diff --git a/src/web/public/js/regions/type/rss.js b/src/web/public/js/regions/type/rss.js index da6d5a7..0a8eaa7 100644 --- a/src/web/public/js/regions/type/rss.js +++ b/src/web/public/js/regions/type/rss.js @@ -141,13 +141,14 @@ var hasPreviewBounds = options && options.preview && imageConfig.width && imageConfig.height; if (hasPreviewBounds || imageConfig.verticalAlign) { var wrapperStyle = 'display:inline-flex;align-items:center;justify-content:center;'; + var wrapperClass = hasPreviewBounds ? ' class="slide-preview-image-placeholder-bounds"' : ''; 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 '' + image + ''; + return '' + image + ''; } return image; } diff --git a/src/web/public/js/regions/type/weather.js b/src/web/public/js/regions/type/weather.js index d0228f1..aaf1b9e 100644 --- a/src/web/public/js/regions/type/weather.js +++ b/src/web/public/js/regions/type/weather.js @@ -132,7 +132,7 @@ var style = width ? ' style="display:block;font-size:' + width + 'px;line-height:1;width:' + width + 'px;height:' + height + 'px;"' : ''; var icon = ''; var verticalAlign = alignment === 'top' || alignment === 'bottom' ? alignment : 'middle'; - return width ? '' + icon + '' : icon; + return width ? '' : icon; } return escapeHtml(placeholderUtils.formatPlaceholderValue(placeholderUtils.resolvePlaceholderExpression(value, normalizeWeatherExpression(expression), { timeZone: value.timezone }))); }); diff --git a/src/web/public/js/shared/slide-preview-renderer.js b/src/web/public/js/shared/slide-preview-renderer.js new file mode 100644 index 0000000..b9f5f7e --- /dev/null +++ b/src/web/public/js/shared/slide-preview-renderer.js @@ -0,0 +1,58 @@ +(function (root, factory) { + const renderer = factory(root); + if (typeof module === 'object' && module.exports) { + module.exports = renderer; + } else { + root.pulseSlidePreviewRenderer = renderer; + } +}(typeof window !== 'undefined' ? window : globalThis, function (root) { + function renderRegionContent(region, content, context, registry) { + const regionType = String(region.region_type || content.type || 'text').trim().toLowerCase(); + const definition = registry && typeof registry.get === 'function' ? registry.get(regionType) : null; + if (!definition || typeof definition.renderPreview !== 'function') { + return ''; + } + + if (regionType === 'rtmp') { + return definition.renderPreview(content, content.disable_audio); + } + if (regionType === 'time-date') { + return definition.renderPreview(region, content, content, 1); + } + if (regionType === 'text') { + return definition.renderPreview(region, content, content); + } + return definition.renderPreview(region, content, context); + } + + function render(payload, registry) { + const settings = payload || {}; + const activeRegistry = registry || root.pulseRegionTypes; + const context = { + existingContent: settings.existingContent || {}, + sources: Array.isArray(settings.apiSources) ? settings.apiSources : [], + feeds: Array.isArray(settings.rssFeeds) ? settings.rssFeeds : [], + weatherLocations: Array.isArray(settings.weatherLocations) ? settings.weatherLocations : [], + timetableGroups: Array.isArray(settings.timetableGroups) ? settings.timetableGroups : [] + }; + + return (Array.isArray(settings.regions) ? settings.regions : []).map(function (region) { + const content = region.content && typeof region.content === 'object' ? region.content : { value: region.content || '' }; + const regionType = String(region.region_type || content.type || 'text').trim().toLowerCase(); + const left = Math.max(0, Math.round(Number(region.x) || 0)); + const top = Math.max(0, Math.round(Number(region.y) || 0)); + const width = Math.max(1, Math.round(Number(region.width) || 1)); + const height = Math.max(1, Math.round(Number(region.height) || 1)); + const zIndex = Math.max(1, Number(region.z_index) || 1); + const regionKey = root.pulseRegionUtils && root.pulseRegionUtils.escapeHtml + ? root.pulseRegionUtils.escapeHtml(region.region_key || '') + : String(region.region_key || '').replace(/[&<>"']/g, function (character) { + return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[character]; + }); + const html = renderRegionContent(region, content, context, activeRegistry); + return '
'; + }).join(''); + } + + return { render: render }; +})); \ No newline at end of file diff --git a/src/web/public/js/shared/weather-icons.js b/src/web/public/js/shared/weather-icons.js index 05addab..66a382a 100644 --- a/src/web/public/js/shared/weather-icons.js +++ b/src/web/public/js/shared/weather-icons.js @@ -26,7 +26,7 @@ var iconWidth = width ? width + 'px' : '1em'; var iconHeight = height ? height + 'px' : '1em'; var verticalAlign = alignment === 'top' || alignment === 'bottom' ? alignment : 'middle'; - return '