diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cb0004..16541f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +## 2.20.6 - 2026-10-26 + +### Added + +- Image-only slide previews from thumbnails on the Slides list and playlist add/edit items, using retained full-size captures without loading slide assets and falling back to existing small thumbnails until refreshed. + +### Fixed + +- Slide preview popups refresh the existing window on repeat opens; portrait previews shrink proportionally to fit and remain centered without nested scrolling or overflow. + ## 2.20.5 - 2026-10-25 ### Fixed diff --git a/build/package.player.json b/build/package.player.json index 1e3fa23..1a9cbe2 100644 --- a/build/package.player.json +++ b/build/package.player.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-player", - "version": "2.20.5", + "version": "2.20.6", "private": false, "description": "Pulse Signage player application bundle", "engines": { diff --git a/build/package.web.json b/build/package.web.json index 38747a4..8b15b64 100644 --- a/build/package.web.json +++ b/build/package.web.json @@ -1,6 +1,6 @@ { "name": "pulse-signage-web", - "version": "2.20.5", + "version": "2.20.6", "private": false, "description": "Pulse Signage web and bridge application bundle", "engines": { diff --git a/package-lock.json b/package-lock.json index 0383684..29657cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "pulse-signage", - "version": "2.20.5", + "version": "2.20.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pulse-signage", - "version": "2.20.5", + "version": "2.20.6", "dependencies": { "@better-auth/passkey": "^1.7.13", "animate.css": "^4.1.1", diff --git a/package.json b/package.json index 8226606..30f2fa1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pulse-signage", - "version": "2.20.5", + "version": "2.20.6", "private": false, "description": "Pulse Signage application with MySQL and media storage", "engines": { diff --git a/src/web/handlebars.js b/src/web/handlebars.js index 30ce29d..0f91af6 100644 --- a/src/web/handlebars.js +++ b/src/web/handlebars.js @@ -178,6 +178,7 @@ function registerActionHelpers(Handlebars) { function registerPartials(Handlebars, viewsRoot) { Handlebars.registerPartial('view-as-banner', fs.readFileSync(path.join(viewsRoot, 'shared', 'view-as-banner.hbs'), 'utf8')); Handlebars.registerPartial('modal-shell', fs.readFileSync(path.join(viewsRoot, 'shared', 'modal-shell.hbs'), 'utf8')); + Handlebars.registerPartial('slide-preview-modal', fs.readFileSync(path.join(viewsRoot, 'shared', 'slide-preview-modal.hbs'), 'utf8')); Handlebars.registerPartial('media-picker-modal', fs.readFileSync(path.join(viewsRoot, 'shared', 'media-picker-modal.hbs'), 'utf8')); Handlebars.registerPartial('table-pagination', fs.readFileSync(path.join(viewsRoot, 'shared', 'table', 'table-pagination.hbs'), 'utf8')); Handlebars.registerPartial('playlists/form', fs.readFileSync(path.join(viewsRoot, 'signage', 'playlists', 'form.hbs'), 'utf8')); diff --git a/src/web/lib/media/library.js b/src/web/lib/media/library.js index 5820a94..581f5d6 100644 --- a/src/web/lib/media/library.js +++ b/src/web/lib/media/library.js @@ -253,7 +253,8 @@ async function countMediaAssetReferences(pool, mediaPath) { 'SELECT COUNT(*) AS ref_count FROM c_templates WHERE background_image_path = ?', [normalizedPath] ); - const [thumbnailRows] = await pool.query('SELECT COUNT(*) AS ref_count FROM c_slides WHERE thumbnail_path = ?', [normalizedPath]); + const thumbnailPath = normalizedPath.startsWith('/media/thumbnails/') ? normalizedPath.replace(/\.full\.png$/i, '.png') : normalizedPath; + const [thumbnailRows] = await pool.query('SELECT COUNT(*) AS ref_count FROM c_slides WHERE thumbnail_path = ?', [thumbnailPath]); return contentReferenceCount + Number(templateRows[0] && templateRows[0].ref_count || 0) + Number(thumbnailRows[0] && thumbnailRows[0].ref_count || 0); } diff --git a/src/web/lib/media/slide-thumbnails.js b/src/web/lib/media/slide-thumbnails.js index c96c7ec..9a7ce9a 100644 --- a/src/web/lib/media/slide-thumbnails.js +++ b/src/web/lib/media/slide-thumbnails.js @@ -456,6 +456,7 @@ async function captureSlideThumbnail(options) { const thumbnailRelativePath = String(slide.thumbnail_path || '').trim() || '/media/thumbnails/slides/slide-' + slide.id + '.png'; const filePath = path.join(mediaDir, thumbnailRelativePath.replace(/^\/+media\//, '')); const fullSizePath = filePath.replace(/\.png$/i, '.full.png'); + const fullSizeTempPath = filePath.replace(/\.png$/i, '.full.tmp.png'); const thumbnailTempPath = filePath.replace(/\.png$/i, '.tmp.png'); const thumbnailPath = thumbnailRelativePath; @@ -527,7 +528,7 @@ async function captureSlideThumbnail(options) { throw new Error('Popup preview canvas has no screenshot bounds.'); } await page.screenshot({ - path: fullSizePath, + path: fullSizeTempPath, clip: { x: Math.max(0, canvasBounds.x), y: Math.max(0, canvasBounds.y), @@ -546,7 +547,7 @@ async function captureSlideThumbnail(options) { }); } - await sharp(fullSizePath) + await sharp(fullSizeTempPath) .resize({ width: THUMBNAIL_MAX_SIZE.width, height: THUMBNAIL_MAX_SIZE.height, @@ -558,11 +559,7 @@ async function captureSlideThumbnail(options) { await fs.promises.rm(filePath, { force: true }); await fs.promises.rename(thumbnailTempPath, filePath); - await fs.promises.unlink(fullSizePath).catch(function (error) { - if (!error || error.code !== 'ENOENT') { - throw error; - } - }); + await fs.promises.rename(fullSizeTempPath, fullSizePath); await pool.query('UPDATE c_slides SET thumbnail_path = ? WHERE id = ?', [thumbnailPath, slide.id]); return { diff --git a/src/web/lib/media/upload-sync.js b/src/web/lib/media/upload-sync.js index 7a4df14..b7ca57b 100644 --- a/src/web/lib/media/upload-sync.js +++ b/src/web/lib/media/upload-sync.js @@ -370,9 +370,10 @@ function createUploadSyncService(options) { async function countUploadReferences(pool, uploadPath) { const contentReferenceCount = await countContentReferenceOwners(pool, 'media', uploadPath); + const thumbnailPath = uploadPath.startsWith('/media/thumbnails/') ? uploadPath.replace(/\.full\.png$/i, '.png') : uploadPath; const [thumbnailRows] = await pool.query( 'SELECT COUNT(*) AS ref_count FROM c_slides WHERE thumbnail_path = ?', - [uploadPath] + [thumbnailPath] ); const [templateRows] = await pool.query( 'SELECT COUNT(*) AS ref_count FROM c_templates WHERE background_image_path = ?', diff --git a/src/web/public/css/theme-custom.css b/src/web/public/css/theme-custom.css index da415af..91241d9 100644 --- a/src/web/public/css/theme-custom.css +++ b/src/web/public/css/theme-custom.css @@ -2477,9 +2477,8 @@ td[data-label="Slides"] { padding: 0.875rem; } -.slide-editor-shell .slide-preview-scroll { - max-height: calc(100vh - 8rem); - overflow: auto; +.slide-editor-shell .slide-preview-stage { + max-height: max(12rem, calc(100vh - 22rem)); } .slide-preview-header h3 { @@ -4658,10 +4657,15 @@ table.table > :not(caption) > * > * { width: 100%; height: auto; aspect-ratio: var(--slide-preview-aspect-ratio, 16 / 9); + object-fit: contain; border: 0; background: var(--bs-body-bg); } +.slide-list-preview-frame[hidden] { + display: none; +} + #slide-preview-modal .modal-content { overflow: hidden; } diff --git a/src/web/public/js/playlists/playlist-schedule.js b/src/web/public/js/playlists/playlist-schedule.js index 4087c99..9340d49 100644 --- a/src/web/public/js/playlists/playlist-schedule.js +++ b/src/web/public/js/playlists/playlist-schedule.js @@ -899,7 +899,7 @@ return '' + '
Preview unavailable
+