Implement video region support
This commit is contained in:
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user