Save worktree changes
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
// Capture the current viewport dimensions.
|
||||
function getCurrentViewport() {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
};
|
||||
}
|
||||
|
||||
// Command websocket and player-state helpers.
|
||||
// Send the current playback state to the command websocket.
|
||||
function sendCommandState(currentSlide) {
|
||||
if (!commandSocket || commandSocket.readyState !== WebSocket.OPEN) {
|
||||
return;
|
||||
}
|
||||
commandSocket.send(JSON.stringify({
|
||||
type: 'state',
|
||||
clientId: getCommandClientId(),
|
||||
clientName: getOnboardingClientName() || null,
|
||||
deviceId: getOnboardingDeviceId() || null,
|
||||
userAgent: window.navigator.userAgent || '',
|
||||
page: window.location.href,
|
||||
viewport: getCurrentViewport(),
|
||||
paused: isPaused,
|
||||
blackout: isBlackout,
|
||||
currentSlide: currentSlide ? {
|
||||
id: currentSlide.id || null,
|
||||
title: currentSlide.title || '',
|
||||
kind: currentSlide.kind || '',
|
||||
playlistSignature: currentPlaylistSignature || ''
|
||||
} : null
|
||||
}));
|
||||
}
|
||||
|
||||
// Debounce command-state updates during rapid changes.
|
||||
function scheduleCommandStateUpdate() {
|
||||
if (commandStateTimer) {
|
||||
window.clearTimeout(commandStateTimer);
|
||||
}
|
||||
commandStateTimer = window.setTimeout(function () {
|
||||
commandStateTimer = null;
|
||||
sendCommandState(lastRenderedSlide);
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// Debounce rerenders after viewport changes.
|
||||
function scheduleViewportRenderUpdate() {
|
||||
if (viewportRenderTimer) {
|
||||
window.clearTimeout(viewportRenderTimer);
|
||||
}
|
||||
viewportRenderTimer = window.setTimeout(function () {
|
||||
viewportRenderTimer = null;
|
||||
if (slides.length) {
|
||||
var activeSlides = getCurrentActiveSlides();
|
||||
if (getCurrentRenderKey(activeSlides) === lastRenderedViewKey) {
|
||||
return;
|
||||
}
|
||||
showCurrent();
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
|
||||
// Cancel the current slide-advance timer.
|
||||
function clearSlideTimer() {
|
||||
if (timer) {
|
||||
window.clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule the next slide transition.
|
||||
function scheduleSlideAdvance(delayMs) {
|
||||
clearSlideTimer();
|
||||
var holdDelayMs = Math.max(1, Number(delayMs || 0));
|
||||
slideExpiresAt = Date.now() + holdDelayMs;
|
||||
timer = window.setTimeout(function () {
|
||||
timer = null;
|
||||
slideExpiresAt = null;
|
||||
pausedRemainingMs = null;
|
||||
const activeSlides = getCurrentActiveSlides();
|
||||
if (activeSlides.length < 2) {
|
||||
refresh();
|
||||
return;
|
||||
}
|
||||
if (index >= activeSlides.length) {
|
||||
index = 0;
|
||||
}
|
||||
index = (index + 1) % activeSlides.length;
|
||||
showCurrent();
|
||||
}, holdDelayMs);
|
||||
}
|
||||
|
||||
// Cancel any pending fade-transition cleanup.
|
||||
function clearSlideTransitionTimer() {
|
||||
if (slideTransitionTimer) {
|
||||
window.clearTimeout(slideTransitionTimer);
|
||||
slideTransitionTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Swap slide markup with optional fade animation.
|
||||
function renderSlideMarkup(markup, shouldFade) {
|
||||
clearSlideTransitionTimer();
|
||||
if (typeof destroyRtmpRegions === 'function') {
|
||||
destroyRtmpRegions(app);
|
||||
}
|
||||
if (!shouldFade) {
|
||||
app.innerHTML = markup;
|
||||
if (typeof syncRtmpRegions === 'function') {
|
||||
syncRtmpRegions(app);
|
||||
}
|
||||
return app.firstElementChild;
|
||||
}
|
||||
|
||||
var topLevelChildren = Array.prototype.slice.call(app.children || []);
|
||||
var existingShells = topLevelChildren.filter(function (child) {
|
||||
return child && child.classList && child.classList.contains('slide-shell');
|
||||
});
|
||||
var previousShell = existingShells.length ? existingShells[existingShells.length - 1] : app.firstElementChild;
|
||||
|
||||
if (existingShells.length > 1) {
|
||||
existingShells.slice(0, -1).forEach(function (shell) {
|
||||
if (shell && shell.parentNode) {
|
||||
shell.parentNode.removeChild(shell);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var nextShell = document.createElement('div');
|
||||
nextShell.className = 'slide-shell';
|
||||
nextShell.style.opacity = '0';
|
||||
nextShell.innerHTML = markup;
|
||||
|
||||
if (!previousShell || (previousShell.classList && previousShell.classList.contains('empty'))) {
|
||||
app.innerHTML = '';
|
||||
nextShell.style.opacity = '1';
|
||||
app.appendChild(nextShell);
|
||||
if (typeof syncRtmpRegions === 'function') {
|
||||
syncRtmpRegions(nextShell);
|
||||
}
|
||||
return nextShell;
|
||||
}
|
||||
|
||||
if (!previousShell.classList.contains('slide-shell')) {
|
||||
previousShell.classList.add('slide-shell');
|
||||
}
|
||||
previousShell.style.opacity = '1';
|
||||
|
||||
app.appendChild(nextShell);
|
||||
void nextShell.offsetHeight;
|
||||
window.requestAnimationFrame(function () {
|
||||
nextShell.style.opacity = '1';
|
||||
previousShell.style.opacity = '0';
|
||||
});
|
||||
|
||||
if (typeof syncRtmpRegions === 'function') {
|
||||
syncRtmpRegions(nextShell);
|
||||
}
|
||||
|
||||
slideTransitionTimer = window.setTimeout(function () {
|
||||
if (previousShell && previousShell.parentNode) {
|
||||
previousShell.parentNode.removeChild(previousShell);
|
||||
}
|
||||
if (nextShell) {
|
||||
nextShell.style.opacity = '1';
|
||||
}
|
||||
slideTransitionTimer = null;
|
||||
}, slideFadeDurationMs);
|
||||
|
||||
return nextShell;
|
||||
}
|
||||
|
||||
// Mirror blackout state onto the document body.
|
||||
function syncBlackoutState() {
|
||||
document.body.classList.toggle('screen-blackout', isBlackout);
|
||||
}
|
||||
|
||||
// Apply pause state and preserve remaining slide time.
|
||||
function setPaused(nextPaused) {
|
||||
var normalized = Boolean(nextPaused);
|
||||
if (isPaused === normalized) {
|
||||
return;
|
||||
}
|
||||
if (normalized) {
|
||||
pausedRemainingMs = slideExpiresAt ? Math.max(0, slideExpiresAt - Date.now()) : null;
|
||||
isPaused = true;
|
||||
clearSlideTimer();
|
||||
sendCommandState(lastRenderedSlide);
|
||||
return;
|
||||
}
|
||||
|
||||
isPaused = false;
|
||||
sendCommandState(lastRenderedSlide);
|
||||
if (!slides.length || !lastRenderedSlide) {
|
||||
return;
|
||||
}
|
||||
if (pausedRemainingMs !== null) {
|
||||
scheduleSlideAdvance(pausedRemainingMs);
|
||||
pausedRemainingMs = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply blackout state and notify the server.
|
||||
function setBlackout(nextBlackout) {
|
||||
var normalized = Boolean(nextBlackout);
|
||||
if (isBlackout === normalized) {
|
||||
return;
|
||||
}
|
||||
isBlackout = normalized;
|
||||
syncBlackoutState();
|
||||
sendCommandState(lastRenderedSlide);
|
||||
}
|
||||
|
||||
// Coerce command payload values into booleans or null.
|
||||
function normalizeBoolean(value) {
|
||||
if (value === true || value === false) {
|
||||
return value;
|
||||
}
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
}
|
||||
var normalized = String(value).trim().toLowerCase();
|
||||
if (['1', 'true', 'yes', 'on'].indexOf(normalized) !== -1) {
|
||||
return true;
|
||||
}
|
||||
if (['0', 'false', 'no', 'off', ''].indexOf(normalized) !== -1) {
|
||||
return false;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Move to the previous or next active slide.
|
||||
function navigateSlides(offset) {
|
||||
const manualSlides = getCurrentActiveSlides();
|
||||
if (!manualSlides.length) {
|
||||
return;
|
||||
}
|
||||
let currentIndex = manualSlides.findIndex(function (slide) {
|
||||
return slide && lastRenderedSlide && slide.id === lastRenderedSlide.id;
|
||||
});
|
||||
if (currentIndex < 0) {
|
||||
currentIndex = Math.min(Math.max(index, 0), manualSlides.length - 1);
|
||||
}
|
||||
const nextIndex = (currentIndex + offset + manualSlides.length) % manualSlides.length;
|
||||
clearSlideTimer();
|
||||
applyPendingPlaylistUpdate();
|
||||
renderSlideAtIndex(manualSlides, nextIndex);
|
||||
}
|
||||
|
||||
// Route incoming websocket command messages.
|
||||
function handleCommandMessage(rawMessage) {
|
||||
var payload;
|
||||
try {
|
||||
payload = JSON.parse(String(rawMessage || ''));
|
||||
} catch (_error) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!payload || payload.type !== 'command') {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (payload.command) {
|
||||
case 'refresh':
|
||||
refresh();
|
||||
return;
|
||||
case 'setclientname':
|
||||
if (payload.clientName) {
|
||||
applyOnboardingClientName(payload.clientName, commandSocket);
|
||||
}
|
||||
return;
|
||||
case 'redirect':
|
||||
if (payload.url) {
|
||||
window.location.replace(String(payload.url));
|
||||
}
|
||||
return;
|
||||
case 'pause':
|
||||
setPaused(!isPaused);
|
||||
return;
|
||||
case 'blackout':
|
||||
var desiredBlackout = normalizeBoolean(payload.blackout);
|
||||
if (desiredBlackout !== null) {
|
||||
setBlackout(desiredBlackout);
|
||||
} else {
|
||||
setBlackout(!isBlackout);
|
||||
}
|
||||
return;
|
||||
case 'previous':
|
||||
case 'left':
|
||||
navigateSlides(-1);
|
||||
return;
|
||||
case 'next':
|
||||
case 'right':
|
||||
navigateSlides(1);
|
||||
return;
|
||||
case 'reload':
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Retry the command websocket after a disconnect.
|
||||
function scheduleCommandReconnect() {
|
||||
if (commandReconnectTimer) {
|
||||
return;
|
||||
}
|
||||
commandReconnectTimer = window.setTimeout(function () {
|
||||
commandReconnectTimer = null;
|
||||
connectCommandSocket();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Open and wire the command websocket connection.
|
||||
function connectCommandSocket() {
|
||||
if (!window.WebSocket) {
|
||||
return;
|
||||
}
|
||||
if (commandSocket && (commandSocket.readyState === WebSocket.OPEN || commandSocket.readyState === WebSocket.CONNECTING)) {
|
||||
return;
|
||||
}
|
||||
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
var socketUrl = new URL(commandSocketPath, window.location.origin);
|
||||
if (window.__pulsePageAuthToken) {
|
||||
socketUrl.searchParams.set('auth', window.__pulsePageAuthToken);
|
||||
}
|
||||
var socket = new WebSocket(socketUrl.toString());
|
||||
commandSocket = socket;
|
||||
|
||||
socket.onopen = function () {
|
||||
if (typeof syncOnboardingClientNameFromServer === 'function') {
|
||||
syncOnboardingClientNameFromServer(socket).then(function () {
|
||||
sendCommandHello(socket);
|
||||
});
|
||||
return;
|
||||
}
|
||||
sendCommandHello(socket);
|
||||
};
|
||||
|
||||
socket.onmessage = function (event) {
|
||||
handleCommandMessage(event.data);
|
||||
};
|
||||
|
||||
socket.onclose = function () {
|
||||
commandSocket = null;
|
||||
scheduleCommandReconnect();
|
||||
};
|
||||
|
||||
socket.onerror = function () {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (_error) {
|
||||
// ignore socket close errors
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user