Release 2.20.6: saved-image slide previews
This commit is contained in:
1 parent
205ce42337
commit
24df245ea1
20 files changed
+318
-75
No files matched your search
@@ -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'));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = ?',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -899,7 +899,7 @@
|
||||
return '' +
|
||||
'<td data-label="Slide">' +
|
||||
'<div class="playlist-slide-cell">' +
|
||||
'<div class="playlist-slide-thumb" aria-hidden="true"' + thumbnailStyle + '>' + thumbnailMarkup + '</div>' +
|
||||
'<button type="button" class="playlist-slide-thumb slide-list-thumbnail-button" data-bs-toggle="modal" data-bs-target="#slide-preview-modal" data-slide-preview-url="' + escapeHtmlAttribute(String(values.thumbnail_path || '')) + '" data-slide-preview-title="' + escapeHtmlAttribute(String(values.title || 'Slide')) + '" data-canvas-width="' + escapeHtmlAttribute(String(values.canvas_width || '')) + '" data-canvas-height="' + escapeHtmlAttribute(String(values.canvas_height || '')) + '" aria-label="Preview slide: ' + escapeHtmlAttribute(String(values.title || 'Slide')) + '"' + thumbnailStyle + '>' + thumbnailMarkup + '</button>' +
|
||||
'<div class="playlist-slide-cell-content"><span class="playlist-slide-title">' + values.title + '</span></div>' +
|
||||
'</div>' +
|
||||
'<input type="hidden" name="slide_id[]" value="' + values.slide_id + '" form="playlist-edit-form" />' +
|
||||
@@ -3327,6 +3327,11 @@
|
||||
thumb.style.setProperty('--playlist-slide-thumb-aspect-ratio', canvasWidth + ' / ' + canvasHeight);
|
||||
}
|
||||
if (thumb) {
|
||||
thumb.setAttribute('data-slide-preview-url', thumbnailPath);
|
||||
thumb.setAttribute('data-slide-preview-title', String(values.title || 'Slide'));
|
||||
thumb.setAttribute('data-canvas-width', String(canvasWidth || ''));
|
||||
thumb.setAttribute('data-canvas-height', String(canvasHeight || ''));
|
||||
thumb.setAttribute('aria-label', 'Preview slide: ' + String(values.title || 'Slide'));
|
||||
thumb.innerHTML = thumbnailPath
|
||||
? '<img class="playlist-slide-thumb-image" src="' + escapeHtmlAttribute(thumbnailPath) + '" alt="" loading="lazy" data-playlist-slide-thumb-image />'
|
||||
: '<span class="playlist-slide-thumb-placeholder" data-playlist-slide-thumb-placeholder><i class="bi bi-image" aria-hidden="true"></i></span>';
|
||||
|
||||
@@ -706,6 +706,21 @@ import { createSlideAnimationEditor } from '/assets/js/slides/slide-animation-ed
|
||||
if (!getSelectedTemplate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (previewPopupWindow && !previewPopupWindow.closed) {
|
||||
try {
|
||||
if (previewPopupWindow.location.pathname === '/slides/popup-preview') {
|
||||
previewPopupWindow.location.hash = buildPreviewPopupHash();
|
||||
} else {
|
||||
previewPopupWindow.location.replace(buildPreviewPopupUrl());
|
||||
}
|
||||
} catch (_error) {
|
||||
previewPopupWindow.location.href = buildPreviewPopupUrl();
|
||||
}
|
||||
previewPopupWindow.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
var popupWidth = 1280;
|
||||
var popupHeight = 900;
|
||||
var left = Math.max(0, Math.round((window.screen.width - popupWidth) / 2));
|
||||
@@ -772,6 +787,19 @@ import { createSlideAnimationEditor } from '/assets/js/slides/slide-animation-ed
|
||||
sidebarSyncFrame = window.requestAnimationFrame(syncSlideEditorSidebar);
|
||||
}
|
||||
|
||||
function sizePreviewStage(canvasWidth, canvasHeight) {
|
||||
var stageContainer = slidePreviewStage.parentElement;
|
||||
var containerStyle = window.getComputedStyle(stageContainer);
|
||||
var availableWidth = stageContainer.clientWidth - parseFloat(containerStyle.paddingLeft) - parseFloat(containerStyle.paddingRight);
|
||||
var maxHeight = parseFloat(window.getComputedStyle(slidePreviewStage).maxHeight);
|
||||
if (!availableWidth || !Number.isFinite(maxHeight)) {
|
||||
return;
|
||||
}
|
||||
|
||||
slidePreviewStage.style.width = Math.min(availableWidth, maxHeight * canvasWidth / canvasHeight) + 'px';
|
||||
slidePreviewStage.style.marginInline = 'auto';
|
||||
}
|
||||
|
||||
function renderPreview() {
|
||||
var template = getSelectedTemplate();
|
||||
if (openPreviewPopupButton) openPreviewPopupButton.disabled = !template;
|
||||
@@ -787,6 +815,7 @@ import { createSlideAnimationEditor } from '/assets/js/slides/slide-animation-ed
|
||||
slidePreviewOverlay.style.height = '';
|
||||
slidePreviewOverlay.style.transform = '';
|
||||
slidePreviewStage.style.aspectRatio = '16 / 9';
|
||||
sizePreviewStage(16, 9);
|
||||
applyBackdropStyle(slidePreviewStage, '#111111');
|
||||
return;
|
||||
}
|
||||
@@ -798,6 +827,7 @@ import { createSlideAnimationEditor } from '/assets/js/slides/slide-animation-ed
|
||||
currentPreviewCanvasHeight = canvasHeight;
|
||||
slidePreviewStage.style.aspectRatio = canvasWidth + ' / ' + canvasHeight;
|
||||
slidePreviewStage.style.setProperty('--slide-layout-ratio', String(canvasWidth / canvasHeight));
|
||||
sizePreviewStage(canvasWidth, canvasHeight);
|
||||
applyBackdropStyle(slidePreviewStage, template.background_color || '#111111', template.background_image_path, template.background_gradient);
|
||||
slidePreviewMeta.textContent = canvasWidth + 'x' + canvasHeight;
|
||||
if (!(template.regions || []).length) {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
{{#> modal-shell modalId="slide-preview-modal" modalLabelId="slide-preview-modal-label" modalDialogClass="modal-xl modal-dialog-centered"}}
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="slide-preview-modal-label">Slide preview</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<img class="slide-list-preview-frame" alt="Slide preview" data-slide-preview-image hidden />
|
||||
<p class="empty m-3" data-slide-preview-empty hidden>Preview unavailable</p>
|
||||
</div>
|
||||
{{/modal-shell}}
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var modal = document.getElementById('slide-preview-modal');
|
||||
if (!modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
var image = modal.querySelector('[data-slide-preview-image]');
|
||||
var empty = modal.querySelector('[data-slide-preview-empty]');
|
||||
var title = modal.querySelector('#slide-preview-modal-label');
|
||||
var currentAspectRatio = 16 / 9;
|
||||
var thumbnailUrl = '';
|
||||
|
||||
image.addEventListener('error', function () {
|
||||
if (thumbnailUrl && image.getAttribute('src') !== thumbnailUrl) {
|
||||
image.src = thumbnailUrl;
|
||||
} else {
|
||||
image.hidden = true;
|
||||
image.removeAttribute('src');
|
||||
empty.hidden = false;
|
||||
}
|
||||
});
|
||||
|
||||
function updateModalSize() {
|
||||
var maxWidth = Math.min(window.innerWidth * 0.9, 1140);
|
||||
var maxHeight = window.innerHeight * 0.72;
|
||||
var width = Math.min(maxWidth, maxHeight * currentAspectRatio);
|
||||
modal.style.setProperty('--slide-preview-aspect-ratio', String(currentAspectRatio));
|
||||
modal.style.setProperty('--slide-preview-modal-width', Math.round(width) + 'px');
|
||||
}
|
||||
|
||||
modal.addEventListener('show.bs.modal', function (event) {
|
||||
var trigger = event.relatedTarget;
|
||||
if (!trigger || !image) {
|
||||
return;
|
||||
}
|
||||
var canvasWidth = Number(trigger.getAttribute('data-canvas-width'));
|
||||
var canvasHeight = Number(trigger.getAttribute('data-canvas-height'));
|
||||
currentAspectRatio = canvasWidth > 0 && canvasHeight > 0 ? canvasWidth / canvasHeight : 16 / 9;
|
||||
updateModalSize();
|
||||
thumbnailUrl = trigger.getAttribute('data-slide-preview-url') || '';
|
||||
image.hidden = !thumbnailUrl;
|
||||
empty.hidden = Boolean(thumbnailUrl);
|
||||
image.alt = trigger.getAttribute('data-slide-preview-title') || 'Slide preview';
|
||||
if (thumbnailUrl) {
|
||||
image.src = thumbnailUrl.replace(/\.png$/i, '.full.png');
|
||||
}
|
||||
if (title) {
|
||||
title.textContent = 'Slide preview: ' + (trigger.getAttribute('data-slide-preview-title') || '');
|
||||
}
|
||||
});
|
||||
modal.addEventListener('hidden.bs.modal', function () {
|
||||
thumbnailUrl = '';
|
||||
image.hidden = true;
|
||||
image.removeAttribute('src');
|
||||
empty.hidden = true;
|
||||
});
|
||||
window.addEventListener('resize', function () {
|
||||
if (modal.classList.contains('show')) {
|
||||
updateModalSize();
|
||||
}
|
||||
});
|
||||
}());
|
||||
</script>
|
||||
@@ -89,6 +89,8 @@
|
||||
|
||||
<script type="application/json" id="playlist-add-slide-data">{{json selectableSlides}}</script>
|
||||
|
||||
{{> slide-preview-modal}}
|
||||
|
||||
{{#> modal-shell modalId="playlist-add-slide-modal" modalLabelId="playlist-add-slide-modal-label" modalDialogClass="modal-dialog-centered modal-dialog-scrollable modal-xl" modalBackdropStatic=true modalKeyboardDisabled=true}}
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
|
||||
@@ -11,13 +11,13 @@
|
||||
</td>
|
||||
<td data-label="Slide">
|
||||
<div class="playlist-slide-cell">
|
||||
<div class="playlist-slide-thumb" aria-hidden="true" {{#if slide.canvas_width}}style="--playlist-slide-thumb-aspect-ratio: {{slide.canvas_width}} / {{slide.canvas_height}};"{{/if}} data-playlist-slide-thumb>
|
||||
<button type="button" class="playlist-slide-thumb slide-list-thumbnail-button" data-bs-toggle="modal" data-bs-target="#slide-preview-modal" data-slide-preview-url="{{slide.thumbnail_path}}" data-slide-preview-title="{{slide.title}}" data-canvas-width="{{slide.canvas_width}}" data-canvas-height="{{slide.canvas_height}}" aria-label="Preview slide: {{slide.title}}" {{#if slide.canvas_width}}style="--playlist-slide-thumb-aspect-ratio: {{slide.canvas_width}} / {{slide.canvas_height}};"{{/if}} data-playlist-slide-thumb>
|
||||
{{#if slide.thumbnail_path}}
|
||||
<img class="playlist-slide-thumb-image" src="{{slide.thumbnail_path}}" alt="" loading="lazy" data-playlist-slide-thumb-image />
|
||||
{{else}}
|
||||
<span class="playlist-slide-thumb-placeholder" data-playlist-slide-thumb-placeholder><i class="bi bi-image" aria-hidden="true"></i></span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</button>
|
||||
<div class="playlist-slide-cell-content">
|
||||
<span class="playlist-slide-title" data-playlist-slide-title>{{slide.title}}</span>
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
{{#each slides}}
|
||||
<tr data-table-search-row data-search-text="{{title}} {{template_name}}">
|
||||
<td data-label="Thumbnail">
|
||||
<button type="button" class="playlist-slide-thumb slide-list-thumbnail-button" data-bs-toggle="modal" data-bs-target="#slide-preview-modal" data-slide-preview-url="/api/internal/slide-thumbnails/{{id}}/popup-preview" data-slide-preview-title="{{title}}" data-canvas-width="{{canvas_width}}" data-canvas-height="{{canvas_height}}" aria-label="Preview slide: {{title}}" {{#if canvas_width}}style="--playlist-slide-thumb-aspect-ratio: {{canvas_width}} / {{canvas_height}};"{{/if}}>
|
||||
<button type="button" class="playlist-slide-thumb slide-list-thumbnail-button" data-bs-toggle="modal" data-bs-target="#slide-preview-modal" data-slide-preview-url="{{thumbnail_path}}" data-slide-preview-title="{{title}}" data-canvas-width="{{canvas_width}}" data-canvas-height="{{canvas_height}}" aria-label="Preview slide: {{title}}" {{#if canvas_width}}style="--playlist-slide-thumb-aspect-ratio: {{canvas_width}} / {{canvas_height}};"{{/if}}>
|
||||
{{#if thumbnail_path}}
|
||||
<img class="playlist-slide-thumb-image" src="{{thumbnail_path}}" alt="" loading="lazy" />
|
||||
{{else}}
|
||||
@@ -79,62 +79,9 @@
|
||||
{{> table-pagination pagination=pagination basePath="/slides" alwaysShow=true}}
|
||||
</div>
|
||||
|
||||
{{#> modal-shell modalId="slide-preview-modal" modalLabelId="slide-preview-modal-label" modalDialogClass="modal-xl modal-dialog-centered"}}
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title fs-5" id="slide-preview-modal-label">Slide preview</h2>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<iframe class="slide-list-preview-frame" title="Slide preview" data-slide-preview-frame src="about:blank"></iframe>
|
||||
</div>
|
||||
{{/modal-shell}}
|
||||
{{> slide-preview-modal}}
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var modal = document.getElementById('slide-preview-modal');
|
||||
if (!modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
var frame = modal.querySelector('[data-slide-preview-frame]');
|
||||
var dialog = modal.querySelector('.modal-dialog');
|
||||
var title = modal.querySelector('#slide-preview-modal-label');
|
||||
var currentAspectRatio = 16 / 9;
|
||||
|
||||
function updateModalSize() {
|
||||
var maxWidth = Math.min(window.innerWidth * 0.9, 1140);
|
||||
var maxHeight = window.innerHeight * 0.72;
|
||||
var width = Math.min(maxWidth, maxHeight * currentAspectRatio);
|
||||
modal.style.setProperty('--slide-preview-aspect-ratio', String(currentAspectRatio));
|
||||
modal.style.setProperty('--slide-preview-modal-width', Math.round(width) + 'px');
|
||||
}
|
||||
|
||||
modal.addEventListener('show.bs.modal', function (event) {
|
||||
var trigger = event.relatedTarget;
|
||||
if (!trigger || !frame) {
|
||||
return;
|
||||
}
|
||||
var canvasWidth = Number(trigger.getAttribute('data-canvas-width'));
|
||||
var canvasHeight = Number(trigger.getAttribute('data-canvas-height'));
|
||||
currentAspectRatio = canvasWidth > 0 && canvasHeight > 0 ? canvasWidth / canvasHeight : 16 / 9;
|
||||
updateModalSize();
|
||||
frame.src = trigger.getAttribute('data-slide-preview-url') || 'about:blank';
|
||||
if (title) {
|
||||
title.textContent = 'Slide preview: ' + (trigger.getAttribute('data-slide-preview-title') || '');
|
||||
}
|
||||
});
|
||||
modal.addEventListener('hidden.bs.modal', function () {
|
||||
if (frame) {
|
||||
frame.src = 'about:blank';
|
||||
}
|
||||
});
|
||||
window.addEventListener('resize', function () {
|
||||
if (modal.classList.contains('show')) {
|
||||
updateModalSize();
|
||||
}
|
||||
});
|
||||
}());
|
||||
|
||||
if (typeof window.attachSlideThumbFallbacks === 'function') {
|
||||
window.attachSlideThumbFallbacks(document);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user