Files
pulse-signage/src/web/public/js/settings/media-library.js
T
lzstealth fea29f6a3c
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m18s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 34s
Release v2.13.2
2026-09-11 21:18:54 +01:00

573 lines
22 KiB
JavaScript

const modal = document.getElementById('media-gallery-preview-modal');
const body = modal && modal.querySelector('[data-media-gallery-preview-body]');
const title = modal && modal.querySelector('#media-gallery-preview-modal-label');
const gallery = document.querySelector('.media-gallery');
const search = gallery && gallery.querySelector('[data-media-gallery-search]');
const typeFilter = gallery && gallery.querySelector('[data-media-gallery-type-filter]');
const usageFilter = gallery && gallery.querySelector('[data-media-gallery-usage-filter]');
const sort = gallery && gallery.querySelector('[data-media-gallery-sort]');
const galleryCount = gallery && gallery.querySelector('[data-media-gallery-count]');
const noResults = gallery && gallery.querySelector('[data-media-gallery-no-results]');
const loadMoreWrap = gallery && gallery.querySelector('[data-media-gallery-load-more-wrap]');
const loadMoreButton = gallery && gallery.querySelector('[data-media-gallery-load-more]');
const selectModeButton = gallery && gallery.querySelector('[data-media-gallery-select-mode]');
const deleteSelectedButton = gallery && gallery.querySelector('[data-media-gallery-delete-selected]');
const addMediaButton = gallery && gallery.querySelector('[data-media-library-add]');
let galleryItems = gallery ? Array.from(gallery.querySelectorAll('[data-media-gallery-column]')) : [];
let visibleRowLimit = 4;
let galleryOffset = gallery ? Number(gallery.dataset.mediaGalleryOffset || galleryItems.length) : 0;
let galleryHasMore = gallery ? gallery.dataset.mediaGalleryHasMore === 'true' : false;
let galleryTotal = galleryCount ? Number(galleryCount.textContent || galleryItems.length) : galleryItems.length;
let galleryRefreshSequence = 0;
let selectionMode = false;
const selectedAssetIds = new Set();
const uploadForm = document.querySelector('form[action="/media-library"]');
const uploadInput = document.querySelector('[data-media-library-upload]');
const uploadZone = document.querySelector('[data-media-library-upload-zone]');
const uploadName = document.querySelector('[data-media-library-upload-name]');
const uploadModal = document.getElementById('media-library-upload-modal');
const cropModal = document.getElementById('media-library-crop-modal');
const cropImage = cropModal && cropModal.querySelector('#media-library-crop-image');
const cropApply = cropModal && cropModal.querySelector('[data-media-library-crop-apply]');
const cropCancel = cropModal && cropModal.querySelector('[data-media-library-crop-cancel]');
const cropStatus = cropModal && cropModal.querySelector('#media-library-crop-status');
const cropFrame = cropModal && cropModal.querySelector('.slide-image-cropper-frame');
let cropper = null;
let cropObjectUrl = '';
let cropSourceFile = null;
let cropFlipX = 1;
let cropFlipY = 1;
function getColumnsPerRow() {
return window.innerWidth >= 1200 ? 6 : window.innerWidth >= 992 ? 3 : window.innerWidth >= 576 ? 2 : 1;
}
function showUploadMessage(message) {
if (typeof window.showToast === 'function') {
window.showToast(message, 'warning');
return;
}
window.alert(message);
}
function showModal(target) {
if (window.pulseModal && typeof window.pulseModal.show === 'function') {
window.pulseModal.show(target);
} else if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(target).show();
}
}
function hideModal(target) {
if (window.pulseModal && typeof window.pulseModal.hide === 'function') {
window.pulseModal.hide(target);
} else if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(target).hide();
}
}
if (addMediaButton && uploadModal) {
addMediaButton.addEventListener('click', function () {
showModal(uploadModal);
});
}
function clearCropper() {
if (cropper) {
cropper.destroy();
cropper = null;
}
if (cropObjectUrl) {
URL.revokeObjectURL(cropObjectUrl);
cropObjectUrl = '';
}
cropSourceFile = null;
cropFlipX = 1;
cropFlipY = 1;
if (cropStatus) cropStatus.textContent = '';
if (cropFrame) cropFrame.classList.remove('is-loading');
}
function setCropperActionState(disabled) {
if (!cropModal) return;
cropModal.querySelectorAll('[data-media-library-crop-action]').forEach(function (button) {
button.disabled = Boolean(disabled);
});
}
function appendGalleryAsset(asset) {
if (!gallery || !asset) return;
const column = document.createElement('div');
column.className = 'col-12 col-sm-6 col-lg-4 col-xl-2';
column.dataset.mediaGalleryColumn = '';
column.dataset.mediaId = String(asset.id || '');
column.dataset.mediaType = asset.media_type || '';
column.dataset.mediaUsed = asset.inUse ? 'true' : 'false';
const article = document.createElement('article');
article.className = 'media-gallery-card';
article.dataset.mediaCreatedAt = asset.created_at || '';
article.dataset.mediaFileSize = asset.file_size || '0';
const preview = document.createElement('button');
preview.type = 'button';
preview.className = 'media-gallery-preview';
preview.dataset.mediaGalleryPreview = '';
preview.dataset.mediaPath = asset.media_path || '';
preview.dataset.mediaType = asset.media_type || '';
preview.dataset.mediaName = asset.original_name || '';
preview.setAttribute('aria-label', 'Preview ' + (asset.original_name || 'media'));
const media = asset.media_type === 'video' ? document.createElement('video') : document.createElement('img');
media.src = asset.media_path || '';
if (asset.media_type === 'video') {
media.muted = true;
media.playsInline = true;
media.preload = 'metadata';
} else {
media.alt = asset.original_name || '';
media.loading = 'lazy';
}
preview.appendChild(media);
if (asset.media_type === 'video') {
const play = document.createElement('span');
play.className = 'media-gallery-play';
play.setAttribute('aria-hidden', 'true');
play.innerHTML = '<i class="bi bi-play-fill"></i>';
preview.appendChild(play);
}
const format = document.createElement('span');
format.className = 'media-gallery-type';
format.textContent = asset.mediaFormat || '';
preview.appendChild(format);
const details = document.createElement('div');
details.className = 'media-gallery-details';
const header = document.createElement('div');
header.className = 'media-gallery-header';
const name = document.createElement('h4');
name.className = 'media-gallery-name';
name.title = asset.original_name || '';
name.textContent = asset.original_name || '';
header.appendChild(name);
if (asset.canDelete) {
if (asset.inUse) {
const lock = document.createElement('button');
lock.type = 'button';
lock.className = 'btn btn-outline-secondary btn-sm media-gallery-action';
lock.disabled = true;
lock.title = 'Media is in use';
lock.innerHTML = '<i class="bi bi-lock" aria-hidden="true"></i><span class="visually-hidden">Media is in use</span>';
header.appendChild(lock);
} else {
const form = document.createElement('form');
form.method = 'post';
form.action = '/media-library/' + encodeURIComponent(asset.id) + '/delete';
form.dataset.confirmMessage = 'Delete this media?';
form.className = 'media-gallery-action';
form.innerHTML = '<button type="submit" class="btn btn-outline-danger btn-sm" title="Delete media"><i class="bi bi-trash" aria-hidden="true"></i><span class="visually-hidden">Delete media</span></button>';
header.appendChild(form);
}
}
const meta = document.createElement('div');
meta.className = 'media-gallery-meta';
meta.textContent = (asset.displayFileSize || '') + ' · Used ' + Number(asset.referenceCount || 0) + ' time' + (Number(asset.referenceCount || 0) === 1 ? '' : 's');
details.appendChild(header);
details.appendChild(meta);
article.appendChild(preview);
article.appendChild(details);
column.appendChild(article);
gallery.querySelector('.media-gallery-scroll .row').appendChild(column);
galleryItems.push(column);
column.querySelector('.media-gallery-card').classList.toggle('media-gallery-card-selected', selectedAssetIds.has(String(asset.id || '')));
}
function applyCropRatio(value) {
if (!cropper) return;
cropper.setAspectRatio(value === 'free' ? NaN : Number(value.split(':')[0]) / Number(value.split(':')[1]));
cropModal.querySelectorAll('[data-media-library-crop-action="ratio"]').forEach(function (button) {
const active = button.dataset.mediaLibraryCropRatio === value;
button.classList.toggle('active', active);
button.setAttribute('aria-pressed', active ? 'true' : 'false');
});
}
function initMediaCropper() {
if (!cropSourceFile || !cropImage || !window.Cropper || cropper) return;
if (cropFrame) cropFrame.classList.add('is-loading');
cropper = new window.Cropper(cropImage, {
aspectRatio: NaN,
autoCropArea: 1,
background: false,
dragMode: 'move',
initialAspectRatio: NaN,
movable: true,
responsive: true,
rotatable: true,
scalable: true,
viewMode: 1,
zoomOnTouch: true,
zoomOnWheel: true,
ready: function () {
if (cropFrame) cropFrame.classList.remove('is-loading');
if (cropper && cropper.container) {
cropper.container.style.width = '100%';
cropper.container.style.height = '560px';
cropper.container.style.maxHeight = '70vh';
}
const containerData = cropper.getContainerData();
const imageData = cropper.getImageData();
const fitRatio = Math.min(Number(containerData.width || 0) / Number(imageData.naturalWidth || 1), Number(containerData.height || 0) / Number(imageData.naturalHeight || 1), 1);
if (fitRatio > 0) cropper.zoomTo(fitRatio);
applyCropRatio('free');
setCropperActionState(false);
}
});
}
function setUploadFile(file, shouldCrop) {
if (!uploadInput || !file) {
return;
}
const allowedTypes = String(uploadInput.accept || '').split(',').map(function (value) { return value.trim().toLowerCase(); }).filter(Boolean);
const fileType = String(file.type || '').toLowerCase();
const matchesType = allowedTypes.length === 0 || allowedTypes.indexOf(fileType) !== -1;
const maxBytes = fileType.indexOf('video/') === 0 ? Number(uploadInput.dataset.videoMaxBytes) : Number(uploadInput.dataset.imageMaxBytes);
if (!matchesType) {
showUploadMessage('This media type is disabled in Media Uploads settings.');
uploadInput.value = '';
return;
}
if (maxBytes && file.size > maxBytes) {
showUploadMessage('This file is larger than the configured upload limit.');
uploadInput.value = '';
return;
}
const transfer = new DataTransfer();
transfer.items.add(file);
uploadInput.files = transfer.files;
if (uploadName) {
uploadName.textContent = file.name;
}
if (shouldCrop && fileType.indexOf('image/') === 0 && cropModal && cropImage && window.Cropper) {
clearCropper();
setCropperActionState(true);
cropSourceFile = file;
cropObjectUrl = URL.createObjectURL(file);
cropImage.src = cropObjectUrl;
if (cropFrame) cropFrame.classList.add('is-loading');
showModal(cropModal);
}
}
function submitUpload() {
if (!uploadForm) {
return;
}
if (typeof uploadForm.requestSubmit === 'function') {
uploadForm.requestSubmit();
} else {
uploadForm.submit();
}
}
if (uploadInput) {
uploadInput.addEventListener('change', function () {
const file = uploadInput.files && uploadInput.files[0];
if (file) {
setUploadFile(file, true);
if (String(file.type || '').toLowerCase().indexOf('image/') !== 0 || !(cropModal && cropImage && window.Cropper)) {
submitUpload();
}
}
});
}
if (uploadZone) {
uploadZone.addEventListener('dragover', function (event) {
event.preventDefault();
uploadZone.classList.add('is-dragover');
});
uploadZone.addEventListener('dragleave', function () {
uploadZone.classList.remove('is-dragover');
});
uploadZone.addEventListener('drop', function (event) {
event.preventDefault();
uploadZone.classList.remove('is-dragover');
const file = event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0];
if (file) {
setUploadFile(file, true);
if (String(file.type || '').toLowerCase().indexOf('image/') !== 0 || !(cropModal && cropImage && window.Cropper)) {
submitUpload();
}
}
});
}
if (cropApply) {
cropApply.addEventListener('click', function () {
if (!cropper || !cropSourceFile || !uploadInput) {
hideModal(cropModal);
return;
}
const outputType = cropSourceFile.type === 'image/jpeg' || cropSourceFile.type === 'image/webp' ? cropSourceFile.type : 'image/png';
cropper.getCroppedCanvas().toBlob(function (blob) {
if (!blob) {
showUploadMessage('Unable to crop this image.');
return;
}
const extension = outputType === 'image/jpeg' ? '.jpg' : outputType === 'image/webp' ? '.webp' : '.png';
const croppedFile = new File([blob], cropSourceFile.name.replace(/\.[^.]+$/, '') + extension, { type: outputType, lastModified: Date.now() });
clearCropper();
setUploadFile(croppedFile, false);
hideModal(cropModal);
submitUpload();
}, outputType, 0.92);
});
}
if (cropCancel) {
cropCancel.addEventListener('click', function () {
clearCropper();
if (uploadInput) uploadInput.value = '';
if (uploadName) uploadName.textContent = 'No file selected';
hideModal(cropModal);
});
}
if (cropModal) {
cropModal.addEventListener('click', function (event) {
const button = event.target && event.target.closest ? event.target.closest('[data-media-library-crop-action]') : null;
if (!button || !cropper) return;
const action = button.dataset.mediaLibraryCropAction;
if (action === 'rotate-left') cropper.rotate(-90);
if (action === 'rotate-right') cropper.rotate(90);
if (action === 'flip-horizontal') {
cropFlipX *= -1;
cropper.scaleX(cropFlipX);
}
if (action === 'flip-vertical') {
cropFlipY *= -1;
cropper.scaleY(cropFlipY);
}
if (action === 'ratio') applyCropRatio(button.dataset.mediaLibraryCropRatio || 'free');
if (action === 'reset') {
cropper.reset();
cropFlipX = 1;
cropFlipY = 1;
applyCropRatio('free');
}
});
}
if (cropModal) {
cropModal.addEventListener('shown.bs.modal', function () {
if (cropSourceFile && cropImage && window.Cropper && !cropper) {
initMediaCropper();
}
});
cropModal.addEventListener('hidden.bs.modal', function () {
clearCropper();
});
}
if (uploadForm) {
uploadForm.addEventListener('submit', function (event) {
if (uploadInput && !uploadInput.files.length) {
event.preventDefault();
showUploadMessage('Choose an image or video to upload.');
}
});
}
function getGalleryQuery(offset, limit) {
const params = new URLSearchParams();
params.set('offset', String(offset));
params.set('limit', String(limit));
params.set('search', String(search && search.value || '').trim());
params.set('type', typeFilter ? typeFilter.value : 'all');
params.set('usage', usageFilter ? usageFilter.value : 'all');
params.set('sort', sort ? sort.value : 'date-desc');
return params.toString();
}
function updateSearchResults() {
const columnsPerRow = getColumnsPerRow();
const visibleItemLimit = visibleRowLimit * columnsPerRow;
let visibleCount = 0;
galleryItems.forEach(function (card) {
card.hidden = visibleCount >= visibleItemLimit;
if (!card.hidden) {
visibleCount += 1;
}
});
if (galleryCount) {
galleryCount.textContent = String(galleryTotal);
}
if (noResults) {
noResults.hidden = galleryTotal > 0;
}
if (loadMoreWrap) {
loadMoreWrap.hidden = !galleryHasMore || galleryTotal === 0;
}
updateBulkDeleteActions();
}
function updateBulkDeleteActions() {
if (selectModeButton) {
selectModeButton.setAttribute('aria-pressed', selectionMode ? 'true' : 'false');
selectModeButton.textContent = selectionMode ? 'Cancel' : 'Select';
}
if (deleteSelectedButton) {
deleteSelectedButton.hidden = !selectionMode;
deleteSelectedButton.disabled = selectedAssetIds.size === 0;
}
galleryItems.forEach(function (item) {
const id = String(item.dataset.mediaId || '');
const card = item.querySelector('.media-gallery-card');
if (card) {
card.classList.toggle('media-gallery-card-selected', selectionMode && selectedAssetIds.has(id));
card.classList.toggle('media-gallery-card-selection-disabled', selectionMode && item.dataset.mediaUsed === 'true');
}
});
}
function clearSelectedAssets() {
selectedAssetIds.clear();
}
function setSelectionMode(enabled) {
selectionMode = Boolean(enabled);
if (!selectionMode) clearSelectedAssets();
if (gallery) gallery.classList.toggle('media-gallery-selection-mode', selectionMode);
updateBulkDeleteActions();
}
function submitBulkDelete() {
if (!selectedAssetIds.size || !window.confirm('Delete ' + selectedAssetIds.size + ' selected media asset' + (selectedAssetIds.size === 1 ? '' : 's') + '?')) {
return;
}
const form = document.createElement('form');
form.method = 'post';
form.action = '/media-library/delete';
selectedAssetIds.forEach(function (id) {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'assetIds[]';
input.value = id;
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
async function refreshGallery() {
const requestSequence = ++galleryRefreshSequence;
try {
const response = await fetch('/media-library/assets?' + getGalleryQuery(0, 4 * getColumnsPerRow()), { headers: { Accept: 'application/json' } });
if (!response.ok) throw new Error('Unable to refresh media.');
const result = await response.json();
if (requestSequence !== galleryRefreshSequence) return;
galleryItems.forEach(function (item) { item.remove(); });
galleryItems = [];
clearSelectedAssets();
(result.assets || []).forEach(appendGalleryAsset);
galleryOffset = Number(result.nextOffset || 0);
galleryHasMore = Boolean(result.hasMore);
galleryTotal = Number(result.total || 0);
visibleRowLimit = 4;
updateSearchResults();
} catch (error) {
showUploadMessage(error.message || 'Unable to refresh media.');
}
}
function resetVisibleItemLimit() {
visibleRowLimit = 4;
refreshGallery();
}
if (search) {
search.addEventListener('input', resetVisibleItemLimit);
}
if (typeFilter) typeFilter.addEventListener('change', resetVisibleItemLimit);
if (usageFilter) usageFilter.addEventListener('change', resetVisibleItemLimit);
if (sort) sort.addEventListener('change', resetVisibleItemLimit);
if (gallery) {
gallery.addEventListener('click', function (event) {
if (!selectionMode) return;
const column = event.target && event.target.closest ? event.target.closest('[data-media-gallery-column]') : null;
if (!column || !gallery.contains(column)) return;
event.preventDefault();
event.stopPropagation();
if (column.dataset.mediaUsed === 'true') {
showUploadMessage('This media is in use and cannot be selected.');
return;
}
const id = String(column.dataset.mediaId || '');
if (!id) return;
if (selectedAssetIds.has(id)) selectedAssetIds.delete(id);
else selectedAssetIds.add(id);
updateBulkDeleteActions();
});
}
if (selectModeButton) selectModeButton.addEventListener('click', function () { setSelectionMode(!selectionMode); });
if (deleteSelectedButton) deleteSelectedButton.addEventListener('click', submitBulkDelete);
if (loadMoreButton) {
loadMoreButton.addEventListener('click', async function () {
loadMoreButton.disabled = true;
try {
const response = await fetch('/media-library/assets?' + getGalleryQuery(galleryOffset, 4 * getColumnsPerRow()), { headers: { Accept: 'application/json' } });
if (!response.ok) throw new Error('Unable to load more media.');
const result = await response.json();
(result.assets || []).forEach(appendGalleryAsset);
galleryOffset = Number(result.nextOffset || galleryOffset);
galleryHasMore = Boolean(result.hasMore);
galleryTotal = Number(result.total || galleryTotal);
visibleRowLimit += 4;
updateSearchResults();
} catch (error) {
showUploadMessage(error.message || 'Unable to load more media.');
} finally {
loadMoreButton.disabled = false;
}
});
}
updateSearchResults();
function showPreviewModal() {
if (window.pulseModal && typeof window.pulseModal.show === 'function') {
window.pulseModal.show(modal);
} else if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modal).show();
}
}
if (modal && body) {
document.addEventListener('click', function (event) {
const preview = event.target && event.target.closest ? event.target.closest('[data-media-gallery-preview]') : null;
if (!preview) {
return;
}
const path = preview.getAttribute('data-media-path') || '';
const type = preview.getAttribute('data-media-type') || 'image';
const name = preview.getAttribute('data-media-name') || 'Media preview';
body.replaceChildren();
if (title) {
title.textContent = name;
}
const media = document.createElement(type === 'video' ? 'video' : 'img');
media.src = path;
media.alt = type === 'video' ? '' : name;
if (type === 'video') {
media.controls = true;
media.playsInline = true;
}
body.appendChild(media);
showPreviewModal();
});
modal.addEventListener('hidden.bs.modal', function () {
body.replaceChildren();
});
}