Implement video region support

This commit is contained in:
2026-07-26 14:54:50 +01:00
parent 07ec17ad91
commit eb1f33d82f
30 changed files with 1243 additions and 134 deletions
@@ -1,4 +1,4 @@
(function () {
(function () {
var DAY_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
function formatDays(daysValue) {
@@ -317,6 +317,9 @@
var slidePickerCards = [];
var slidePickerSelection = new Set();
var slidePickerSlides = [];
var videoDurationCache = Object.create(null);
var lastDurationPointerDown = null;
var lastDurationPointerUp = null;
if (!tbody || !form) {
return;
@@ -547,6 +550,11 @@
canvas_width: slide.canvas_width,
canvas_height: slide.canvas_height,
canvas_signature: String(slide.canvasSignature || ''),
showVideoDurationButton: slide.showVideoDurationButton,
videoSourcePath: slide.videoSourcePath,
videoDurationSeconds: slide.videoDurationSeconds,
media_type: slide.media_type,
media_path: slide.media_path,
title: slide.title || 'Slide',
duration_seconds: 10,
schedule_mode: 'always',
@@ -786,6 +794,14 @@
row.setAttribute('data-row-key', rowKey);
row.setAttribute('data-slide-id', String(values.slide_id));
row.setAttribute('data-canvas-signature', String(values.canvas_signature || ''));
row.setAttribute('data-media-type', String(values.media_type || ''));
row.setAttribute('data-media-path', String(values.media_path || ''));
row.setAttribute('data-video-source-path', String(values.videoSourcePath || values.media_path || ''));
row.setAttribute('data-video-duration-seconds', String(values.videoDurationSeconds || ''));
row.setAttribute('data-use-video-duration', String(Boolean(values.useVideoDuration)));
var durationActionMarkup = Boolean(values.showVideoDurationButton) || String(values.media_type || '').trim() === 'video'
? '<button type="button" class="btn btn-outline-secondary btn-sm playlist-use-video-duration' + (values.useVideoDuration ? ' active' : '') + '" data-use-video-duration-button aria-pressed="' + (values.useVideoDuration ? 'true' : 'false') + '">' + (values.useVideoDuration ? 'Use Video Duration' : 'Use video duration') + '</button>'
: '';
row.innerHTML = '' +
'<td class="playlist-order-cell" data-label="Order">' +
'<div class="playlist-order-cell-inner">' +
@@ -808,7 +824,11 @@
'<input type="hidden" name="schedule_end_time[]" value="' + values.schedule_end_time + '" form="playlist-edit-form" />' +
'<input type="hidden" name="schedule_days_json[]" value="' + values.schedule_days_json + '" form="playlist-edit-form" />' +
'</td>' +
'<td><input name="duration_seconds[]" type="number" min="1" value="' + values.duration_seconds + '" required form="playlist-edit-form" class="form-control form-control-sm playlist-duration-input text-end" /></td>' +
'<td><div class="playlist-duration-field"><input name="duration_seconds[]" type="text" inputmode="decimal" value="' + values.duration_seconds + '" required form="playlist-edit-form" class="form-control form-control-sm playlist-duration-input text-end"' + (values.useVideoDuration ? ' disabled' : '') + ' />' +
'<input type="hidden" name="use_video_duration[]" value="' + (values.useVideoDuration ? '1' : '0') + '" form="playlist-edit-form" />' +
(values.useVideoDuration ? '<input type="hidden" name="duration_seconds[]" value="' + values.duration_seconds + '" form="playlist-edit-form" data-video-duration-mirror />' : '') +
durationActionMarkup +
'</div></td>' +
'<td><div class="actions playlist-item-actions">' +
'<button type="button" class="btn btn-sm btn-primary" data-schedule-config="/playlists/' + encodeURIComponent(playlistId) + '/slides/0/config" data-schedule-config-row="' + rowKey + '">Schedule</button>' +
'<button type="button" class="btn btn-sm btn-danger" data-playlist-remove-row>Remove</button>' +
@@ -816,9 +836,202 @@
return row;
}
function loadVideoDuration(mediaPath) {
var cacheKey = String(mediaPath || '').trim();
var cachedPromise;
if (!cacheKey) {
return Promise.reject(new Error('No video source is available for this slide.'));
}
cachedPromise = videoDurationCache[cacheKey];
if (cachedPromise) {
return cachedPromise;
}
cachedPromise = new Promise(function (resolve, reject) {
var video = document.createElement('video');
var timeoutId = window.setTimeout(function () {
cleanup();
reject(new Error('Timed out loading the video metadata.'));
}, 15000);
function cleanup() {
window.clearTimeout(timeoutId);
video.removeAttribute('src');
video.load();
if (video.parentNode) {
video.parentNode.removeChild(video);
}
}
video.preload = 'metadata';
video.muted = true;
video.playsInline = true;
video.setAttribute('playsinline', '');
video.setAttribute('muted', '');
video.style.position = 'absolute';
video.style.left = '-9999px';
video.style.top = '0';
video.style.width = '1px';
video.style.height = '1px';
video.style.opacity = '0';
video.style.pointerEvents = 'none';
video.addEventListener('loadedmetadata', function () {
var duration = Number(video.duration);
cleanup();
if (Number.isFinite(duration) && duration > 0) {
resolve(Math.round(duration * 1000) / 1000);
} else {
reject(new Error('The video duration could not be read.'));
}
}, { once: true });
video.addEventListener('error', function () {
cleanup();
reject(new Error('The video duration could not be read.'));
}, { once: true });
video.addEventListener('abort', function () {
cleanup();
reject(new Error('The video duration could not be read.'));
}, { once: true });
document.body.appendChild(video);
video.src = cacheKey;
video.load();
}).catch(function (error) {
delete videoDurationCache[cacheKey];
throw error;
});
videoDurationCache[cacheKey] = cachedPromise;
return cachedPromise;
}
function setVideoDurationToggleState(row, button, isPressed) {
var input = row ? row.querySelector('[name="duration_seconds[]"]') : null;
var flag = row ? row.querySelector('[name="use_video_duration[]"]') : null;
var mirror = row ? row.querySelector('[data-video-duration-mirror]') : null;
if (row && input && isPressed && !row.getAttribute('data-video-duration-previous-value')) {
row.setAttribute('data-video-duration-previous-value', String(input.value || ''));
}
if (button) {
button.classList.toggle('active', Boolean(isPressed));
button.setAttribute('aria-pressed', isPressed ? 'true' : 'false');
button.textContent = isPressed ? 'Use Video Duration' : 'Use video duration';
}
if (input) {
if (!isPressed && row) {
var previousValue = String(row.getAttribute('data-video-duration-previous-value') || '').trim();
if (previousValue) {
input.value = previousValue;
}
row.removeAttribute('data-video-duration-previous-value');
}
input.disabled = Boolean(isPressed);
input.classList.toggle('playlist-duration-input-locked', Boolean(isPressed));
}
if (flag) {
flag.value = isPressed ? '1' : '0';
}
if (row) {
row.setAttribute('data-use-video-duration', isPressed ? 'true' : 'false');
}
if (!row || !input) {
return;
}
if (isPressed) {
if (!mirror) {
mirror = document.createElement('input');
mirror.type = 'hidden';
mirror.setAttribute('data-video-duration-mirror', '');
input.parentNode.insertBefore(mirror, input.nextSibling);
}
mirror.name = input.name;
mirror.value = input.value;
if (input.getAttribute('form')) {
mirror.setAttribute('form', input.getAttribute('form'));
}
} else if (mirror && mirror.parentNode) {
mirror.parentNode.removeChild(mirror);
}
}
async function applyVideoDurationToRow(row, button) {
var mediaType = String(row && row.getAttribute('data-media-type') || '').trim().toLowerCase();
var mediaPath = String(row && (row.getAttribute('data-video-source-path') || row.getAttribute('data-media-path')) || '').trim();
var input = row ? row.querySelector('[name="duration_seconds[]"]') : null;
var isVideoRow = mediaType === 'video' || Boolean(mediaPath);
var storedDuration = Number(row && row.getAttribute('data-video-duration-seconds') || 0);
if (!isVideoRow || !input) {
return;
}
if (button && button.getAttribute('aria-pressed') === 'true') {
setVideoDurationToggleState(row, button, false);
return;
}
setVideoDurationToggleState(row, button, true);
if (Number.isFinite(storedDuration) && storedDuration > 0) {
input.value = String(storedDuration);
var existingMirror = row ? row.querySelector('[data-video-duration-mirror]') : null;
if (existingMirror) {
existingMirror.value = input.value;
}
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
return;
}
if (button) {
button.disabled = true;
button.textContent = 'Loading...';
}
try {
var durationSeconds = await loadVideoDuration(mediaPath);
if (row) {
row.setAttribute('data-video-duration-seconds', String(durationSeconds));
}
input.value = String(durationSeconds);
var mirror = row ? row.querySelector('[data-video-duration-mirror]') : null;
if (mirror) {
mirror.value = input.value;
}
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
} catch (error) {
setVideoDurationToggleState(row, button, false);
alert(error && error.message ? error.message : 'Unable to read the video duration.');
} finally {
if (button) {
button.disabled = false;
button.textContent = button.getAttribute('aria-pressed') === 'true' ? 'Use Video Duration' : 'Use video duration';
}
}
}
tbody.addEventListener('click', function (event) {
if (event.target && event.target.matches('[name="duration_seconds[]"]')) {
event.stopPropagation();
return;
}
var removeButton = event.target.closest('[data-playlist-remove-row]');
var scheduleButton = event.target.closest('[data-schedule-config]');
var durationButton = event.target.closest('[data-use-video-duration]');
var row = event.target.closest('tr[data-playlist-slide-row]');
if (!row) {
@@ -837,6 +1050,102 @@
if (typeof window.openScheduleModal === 'function') {
window.openScheduleModal(scheduleButton.getAttribute('data-schedule-config') + '?row_key=' + encodeURIComponent(row.getAttribute('data-row-key') || ''));
}
return;
}
if (durationButton) {
var rowKey = row.getAttribute('data-row-key');
var pressedOnInput = lastDurationPointerDown && lastDurationPointerDown.type === 'input' && lastDurationPointerDown.rowKey === rowKey;
var releasedOnButton = lastDurationPointerUp && lastDurationPointerUp.type === 'button' && lastDurationPointerUp.rowKey === rowKey;
if (event.detail > 0 && (pressedOnInput || !releasedOnButton)) {
lastDurationPointerDown = null;
lastDurationPointerUp = null;
event.preventDefault();
return;
}
event.preventDefault();
void applyVideoDurationToRow(row, durationButton);
}
lastDurationPointerDown = null;
lastDurationPointerUp = null;
});
tbody.addEventListener('mousedown', function (event) {
var row = event.target && event.target.closest ? event.target.closest('tr[data-playlist-slide-row]') : null;
var dragHandle = event.target && event.target.closest ? event.target.closest('[data-playlist-drag-handle]') : null;
var durationInput = event.target && event.target.matches('[name="duration_seconds[]"]') ? event.target : null;
var durationToggle = event.target && event.target.closest ? event.target.closest('.playlist-use-video-duration') : null;
if (row && !dragHandle) {
event.stopPropagation();
}
if (durationInput) {
lastDurationPointerDown = {
type: 'input',
rowKey: String(event.target.closest('tr[data-playlist-slide-row]') && event.target.closest('tr[data-playlist-slide-row]').getAttribute('data-row-key') || '')
};
event.stopPropagation();
return;
}
if (durationToggle) {
lastDurationPointerDown = {
type: 'button',
rowKey: String(durationToggle.closest('tr[data-playlist-slide-row]') && durationToggle.closest('tr[data-playlist-slide-row]').getAttribute('data-row-key') || '')
};
return;
}
lastDurationPointerDown = null;
});
tbody.addEventListener('mouseup', function (event) {
var durationInput = event.target && event.target.matches('[name="duration_seconds[]"]') ? event.target : null;
var durationToggle = event.target && event.target.closest ? event.target.closest('.playlist-use-video-duration') : null;
var row = event.target && event.target.closest ? event.target.closest('tr[data-playlist-slide-row]') : null;
if (durationInput) {
lastDurationPointerUp = {
type: 'input',
rowKey: String(row && row.getAttribute('data-row-key') || '')
};
return;
}
if (durationToggle) {
lastDurationPointerUp = {
type: 'button',
rowKey: String(row && row.getAttribute('data-row-key') || '')
};
return;
}
lastDurationPointerUp = null;
});
tbody.addEventListener('input', function (event) {
if (event.target && event.target.matches('[name="duration_seconds[]"]')) {
var editedRow = event.target.closest('tr[data-playlist-slide-row]');
var editedButton = editedRow ? editedRow.querySelector('.playlist-use-video-duration') : null;
var editedMirror = editedRow ? editedRow.querySelector('[data-video-duration-mirror]') : null;
var useVideoDuration = Boolean(editedButton && editedButton.getAttribute('aria-pressed') === 'true');
if (editedRow) {
editedRow.setAttribute('data-use-video-duration', useVideoDuration ? 'true' : 'false');
}
if (useVideoDuration && editedMirror) {
editedMirror.value = event.target.value;
}
if (editedMirror && !useVideoDuration) {
editedMirror.parentNode.removeChild(editedMirror);
}
markPlaylistDirty();
}
});