1298 lines
54 KiB
JavaScript
1298 lines
54 KiB
JavaScript
(function () {
|
|
var webUiHelpers = window.webUiHelpers || {};
|
|
var escapeHtml = webUiHelpers.escapeHtml;
|
|
var formatDashboardDate = webUiHelpers.formatDashboardDate;
|
|
var getClientRowKey = webUiHelpers.getClientRowKey;
|
|
var getClientDisplayName = webUiHelpers.getClientDisplayName;
|
|
var setButtonVariant = webUiHelpers.setButtonVariant;
|
|
var normalizeDisplayIp = webUiHelpers.normalizeDisplayIp;
|
|
var LIST_PAGE_SIZE = 25;
|
|
var CLIENT_ROW_REMOVAL_DELAY_MS = 1000;
|
|
var latestDashboardState = null;
|
|
var pendingClientRowRemovals = {};
|
|
|
|
function getClientSearchInput() {
|
|
var table = document.getElementById('dashboard-clients-table');
|
|
var container = table && typeof table.closest === 'function'
|
|
? (table.closest('[data-table-search-container]') || table.closest('.card') || null)
|
|
: null;
|
|
|
|
return container && container.querySelector ? container.querySelector('[data-table-search]') : document.querySelector('[data-table-search]');
|
|
}
|
|
|
|
function getScreenCommandSelect() {
|
|
return document.querySelector('[data-screen-command-select]');
|
|
}
|
|
|
|
function getScreenCommandForms() {
|
|
if (!document.querySelectorAll) {
|
|
return [];
|
|
}
|
|
|
|
return Array.prototype.slice.call(document.querySelectorAll('[data-screen-command-form]'));
|
|
}
|
|
|
|
function getSelectedScreenClients(state) {
|
|
var select = getScreenCommandSelect();
|
|
var selectedSlug = select ? String(select.value || '').trim() : '';
|
|
var clients = state && Array.isArray(state.clients) ? state.clients : [];
|
|
|
|
if (!selectedSlug || selectedSlug === '__all__') {
|
|
return clients;
|
|
}
|
|
|
|
return clients.filter(function (client) {
|
|
return String(client && client.screen_slug || '').trim() === selectedSlug;
|
|
});
|
|
}
|
|
|
|
function getSelectedScreenLabel() {
|
|
var select = getScreenCommandSelect();
|
|
if (!select) {
|
|
return '';
|
|
}
|
|
|
|
var selectedOption = select.options && select.selectedIndex >= 0 ? select.options[select.selectedIndex] : null;
|
|
return selectedOption ? String(selectedOption.getAttribute('data-screen-name') || selectedOption.textContent || '').trim() : '';
|
|
}
|
|
|
|
function updateToggleButton(button, form, state) {
|
|
if (!button) {
|
|
return;
|
|
}
|
|
|
|
var action = String(form && form.getAttribute('data-screen-command-action') || '').trim();
|
|
var selectedLabel = getSelectedScreenLabel() || 'selected screen group';
|
|
var isAllScreens = String(getScreenCommandSelect() && getScreenCommandSelect().value || '').trim() === '__all__';
|
|
var clients = getSelectedScreenClients(state);
|
|
var hasClients = clients.length > 0;
|
|
var allPaused = hasClients && clients.every(function (client) {
|
|
return Boolean(client && client.paused);
|
|
});
|
|
var allBlackout = hasClients && clients.every(function (client) {
|
|
return Boolean(client && client.blackout);
|
|
});
|
|
|
|
if (action === 'pause') {
|
|
var pauseLabel = allPaused ? 'Resume ' + (isAllScreens ? 'all clients' : 'screen') : 'Pause ' + (isAllScreens ? 'all clients' : 'screen');
|
|
var pauseConfirm = allPaused ? 'Resume ' + (isAllScreens ? 'all connected clients' : selectedLabel) + '?' : 'Pause ' + (isAllScreens ? 'all connected clients' : selectedLabel) + '?';
|
|
var pauseIcon = allPaused ? 'bi-play-fill' : 'bi-pause-fill';
|
|
button.innerHTML = '<i class="bi ' + pauseIcon + ' me-1" aria-hidden="true"></i>' + escapeHtml(pauseLabel);
|
|
setButtonVariant(button, ['btn-success', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], allPaused ? 'btn-success' : 'btn-info');
|
|
button.setAttribute('aria-label', pauseLabel);
|
|
button.setAttribute('title', pauseLabel);
|
|
if (form) {
|
|
var pauseInput = form.querySelector('input[name="paused"]');
|
|
if (pauseInput) {
|
|
pauseInput.value = allPaused ? 'false' : 'true';
|
|
}
|
|
form.setAttribute('data-confirm-message', pauseConfirm);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (action === 'blackout') {
|
|
var blackoutLabel = allBlackout ? 'Restore ' + (isAllScreens ? 'all clients' : 'screen') : 'Blackout ' + (isAllScreens ? 'all clients' : 'screen');
|
|
var blackoutConfirm = allBlackout ? 'Restore ' + (isAllScreens ? 'all connected clients' : selectedLabel) + '?' : 'Blackout ' + (isAllScreens ? 'all connected clients' : selectedLabel) + '?';
|
|
var blackoutIcon = allBlackout ? 'bi-eye' : 'bi-eye-slash';
|
|
button.innerHTML = '<i class="bi ' + blackoutIcon + ' me-1" aria-hidden="true"></i>' + escapeHtml(blackoutLabel);
|
|
setButtonVariant(button, ['btn-success', 'btn-danger', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], allBlackout ? 'btn-success' : 'btn-secondary');
|
|
button.setAttribute('aria-label', blackoutLabel);
|
|
button.setAttribute('title', blackoutLabel);
|
|
if (form) {
|
|
var blackoutInput = form.querySelector('input[name="blackout"]');
|
|
if (blackoutInput) {
|
|
blackoutInput.value = allBlackout ? 'false' : 'true';
|
|
}
|
|
form.setAttribute('data-confirm-message', blackoutConfirm);
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateScreenCommandControls() {
|
|
var select = getScreenCommandSelect();
|
|
if (!select) {
|
|
return;
|
|
}
|
|
|
|
var selectedSlug = String(select.value || '').trim();
|
|
var hasSelectedGroup = Boolean(selectedSlug);
|
|
var actionTarget = '/clients/' + encodeURIComponent(selectedSlug || '__all__') + '/commands';
|
|
var selectedName = getSelectedScreenLabel();
|
|
var selectedClients = getSelectedScreenClients(latestDashboardState);
|
|
var allPaused = selectedClients.length > 0 && selectedClients.every(function (client) {
|
|
return Boolean(client && client.paused);
|
|
});
|
|
var allBlackout = selectedClients.length > 0 && selectedClients.every(function (client) {
|
|
return Boolean(client && client.blackout);
|
|
});
|
|
|
|
getScreenCommandForms().forEach(function (form) {
|
|
if (!form) {
|
|
return;
|
|
}
|
|
|
|
form.setAttribute('action', actionTarget);
|
|
|
|
var action = String(form.getAttribute('data-screen-command-action') || '').trim();
|
|
var button = form.querySelector('button[type="submit"]');
|
|
|
|
if (button) {
|
|
button.disabled = !hasSelectedGroup;
|
|
}
|
|
|
|
if (action === 'pause' || action === 'blackout') {
|
|
updateToggleButton(button, form, latestDashboardState);
|
|
return;
|
|
}
|
|
|
|
if (button) {
|
|
button.setAttribute('aria-label', selectedName ? selectedName : 'Selected screen group');
|
|
}
|
|
});
|
|
}
|
|
|
|
function initScreenCommandControls() {
|
|
var select = getScreenCommandSelect();
|
|
if (!select || (select.dataset && select.dataset.bound === 'true')) {
|
|
return;
|
|
}
|
|
|
|
if (select.dataset) {
|
|
select.dataset.bound = 'true';
|
|
}
|
|
|
|
select.addEventListener('change', updateScreenCommandControls);
|
|
updateScreenCommandControls();
|
|
}
|
|
|
|
function getClientListQueryState() {
|
|
var searchParams = new URLSearchParams(String(window.location && window.location.search || ''));
|
|
var searchInput = getClientSearchInput();
|
|
var searchValue = searchInput ? String(searchInput.value || '').trim() : String(searchParams.get('search') || '').trim();
|
|
|
|
return {
|
|
search: searchValue,
|
|
sort: String(searchParams.get('sort') || '').trim(),
|
|
direction: String(searchParams.get('direction') || '').trim().toLowerCase() === 'desc' ? 'desc' : 'asc',
|
|
page: Math.max(1, Math.floor(Number(searchParams.get('page') || 1) || 1))
|
|
};
|
|
}
|
|
|
|
function isClientSearchLoading() {
|
|
return Boolean(document.querySelector('[data-table-search-loading="true"]'));
|
|
}
|
|
|
|
function getComparableClientSortValue(rawValue) {
|
|
var value = String(rawValue || '').trim();
|
|
|
|
if (!value) {
|
|
return { type: 'empty', value: '' };
|
|
}
|
|
|
|
var numericValue = Number(value.replace(/,/g, ''));
|
|
if (!Number.isNaN(numericValue)) {
|
|
return { type: 'number', value: numericValue };
|
|
}
|
|
|
|
var dateValue = Date.parse(value);
|
|
if (!Number.isNaN(dateValue)) {
|
|
return { type: 'date', value: dateValue };
|
|
}
|
|
|
|
return { type: 'string', value: value.toLowerCase() };
|
|
}
|
|
|
|
function compareClientSortValues(leftValue, rightValue) {
|
|
var left = getComparableClientSortValue(leftValue);
|
|
var right = getComparableClientSortValue(rightValue);
|
|
|
|
if (left.type === 'empty' && right.type === 'empty') {
|
|
return 0;
|
|
}
|
|
if (left.type === 'empty') {
|
|
return 1;
|
|
}
|
|
if (right.type === 'empty') {
|
|
return -1;
|
|
}
|
|
if (left.type === right.type) {
|
|
if (left.value < right.value) {
|
|
return -1;
|
|
}
|
|
if (left.value > right.value) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
return String(left.value).localeCompare(String(right.value), undefined, { numeric: true, sensitivity: 'base' });
|
|
}
|
|
|
|
function createClientSearchMatcher(searchTerm) {
|
|
var query = String(searchTerm || '').trim().toLowerCase();
|
|
|
|
if (!query) {
|
|
return function () {
|
|
return true;
|
|
};
|
|
}
|
|
|
|
return function (client) {
|
|
var searchableValues = [
|
|
client && client.name,
|
|
client && client.client_name,
|
|
client && client.clientId,
|
|
client && client.deviceId,
|
|
client && client.slug,
|
|
client && client.screen_slug,
|
|
client && client.screen_name,
|
|
client && client.status,
|
|
client && client.currentSlideTitle
|
|
];
|
|
|
|
return searchableValues.map(function (value) {
|
|
return String(value || '').trim().toLowerCase();
|
|
}).join(' ').indexOf(query) !== -1;
|
|
};
|
|
}
|
|
|
|
function sortClientsForTable(clients, sortKey, sortDirection) {
|
|
var normalizedSortKey = String(sortKey || '').trim();
|
|
var normalizedDirection = String(sortDirection || '').trim().toLowerCase() === 'desc' ? 'desc' : 'asc';
|
|
var accessors = {
|
|
client: function (client) { return String(client && (getClientDisplayName(client) || client.client_name || client.name || client.clientId) || '').trim(); },
|
|
screen: function (client) { return String(client && (client.screen_name || client.screen_slug) || '').trim(); },
|
|
slide: function (client) { return String(client && client.currentSlideTitle || '').trim(); },
|
|
viewport: function (client) {
|
|
var viewport = client && client.viewport;
|
|
if (!viewport || !viewport.width || !viewport.height) {
|
|
return '';
|
|
}
|
|
return String(Number(viewport.width) || 0) + 'x' + String(Number(viewport.height) || 0);
|
|
},
|
|
connected: function (client) { return String(client && (client.connectedAt || client.lastSeenAt) || '').trim(); }
|
|
};
|
|
|
|
function compareValues(leftValue, rightValue) {
|
|
return compareClientSortValues(leftValue, rightValue);
|
|
}
|
|
|
|
function compareClientNames(leftValue, rightValue) {
|
|
var leftName = String(leftValue || '').trim();
|
|
var rightName = String(rightValue || '').trim();
|
|
if (!leftName && !rightName) {
|
|
return 0;
|
|
}
|
|
if (!leftName) {
|
|
return 1;
|
|
}
|
|
if (!rightName) {
|
|
return -1;
|
|
}
|
|
return leftName.localeCompare(rightName, undefined, { sensitivity: 'base', numeric: true });
|
|
}
|
|
|
|
var sortKeys = accessors[normalizedSortKey]
|
|
? [normalizedSortKey]
|
|
: ['client'];
|
|
|
|
return (Array.isArray(clients) ? clients.slice() : []).sort(function (leftClient, rightClient) {
|
|
for (var index = 0; index < sortKeys.length; index += 1) {
|
|
var sortKeyName = sortKeys[index];
|
|
var comparison = sortKeyName === 'client'
|
|
? compareClientNames(accessors[sortKeyName](leftClient), accessors[sortKeyName](rightClient))
|
|
: compareValues(accessors[sortKeyName](leftClient), accessors[sortKeyName](rightClient));
|
|
|
|
if (comparison !== 0) {
|
|
return index === 0 && normalizedDirection === 'desc' ? comparison * -1 : comparison;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
function getVisibleClients(state) {
|
|
var query = getClientListQueryState();
|
|
var clients = Array.isArray(state && state.clients) ? state.clients.slice() : [];
|
|
var searchMatcher = createClientSearchMatcher(query.search);
|
|
|
|
clients = clients.filter(searchMatcher);
|
|
clients = sortClientsForTable(clients, query.sort, query.direction);
|
|
|
|
return clients.slice((query.page - 1) * LIST_PAGE_SIZE, ((query.page - 1) * LIST_PAGE_SIZE) + LIST_PAGE_SIZE);
|
|
}
|
|
|
|
function renderClientActionCell(client) {
|
|
var paused = Boolean(client.paused);
|
|
var pauseButtonClass = 'btn btn-sm btn-info';
|
|
var pauseButtonIcon = paused ? 'bi-play-fill' : 'bi-pause-fill';
|
|
var pauseButtonLabel = paused ? 'Resume client' : 'Pause client';
|
|
var pauseButtonText = paused ? 'Resume' : 'Pause';
|
|
var blackout = Boolean(client.blackout);
|
|
var blackoutButtonClass = 'btn btn-sm ' + (blackout ? 'btn-success' : 'btn-secondary');
|
|
var blackoutButtonIcon = blackout ? 'bi-eye' : 'bi-eye-slash';
|
|
var blackoutButtonLabel = blackout ? 'Restore client' : 'Blackout client';
|
|
var blackoutButtonText = blackout ? 'Restore' : 'Blackout';
|
|
var reloadConfirmMessage = 'Reloading will restart the player page. Continue?';
|
|
var commandTargetId = client.clientId || client.id || '';
|
|
|
|
return [
|
|
'<div class="actions justify-content-end">',
|
|
'<form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-confirm-message="' + escapeHtml(reloadConfirmMessage) + '" data-async-command><input type="hidden" name="command" value="reload" /><input type="hidden" name="connectionId" value="' + escapeHtml(commandTargetId) + '" /><input type="hidden" name="playerBaseUrl" value="' + escapeHtml(client.player_url || '') + '" /><button type="submit" class="btn btn-sm btn-danger" data-action="reload" aria-label="Reload client" title="Reload client"><i class="bi bi-arrow-repeat" aria-hidden="true"></i></button></form>',
|
|
'<button type="button" class="btn btn-sm btn-danger" data-action="move-screen" aria-label="Move client to another screen" title="Move client to another screen"><i class="bi bi-display" aria-hidden="true"></i></button>',
|
|
'<form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="previous" /><input type="hidden" name="connectionId" value="' + escapeHtml(commandTargetId) + '" /><input type="hidden" name="playerBaseUrl" value="' + escapeHtml(client.player_url || '') + '" /><button type="submit" class="btn btn-sm btn-warning" data-action="previous" aria-label="Previous slide" title="Previous slide"><i class="bi bi-skip-backward-fill" aria-hidden="true"></i></button></form>',
|
|
'<form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="next" /><input type="hidden" name="connectionId" value="' + escapeHtml(commandTargetId) + '" /><input type="hidden" name="playerBaseUrl" value="' + escapeHtml(client.player_url || '') + '" /><button type="submit" class="btn btn-sm btn-warning" data-action="next" aria-label="Next slide" title="Next slide"><i class="bi bi-skip-forward-fill" aria-hidden="true"></i></button></form>',
|
|
'<form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="pause" /><input type="hidden" name="connectionId" value="' + escapeHtml(commandTargetId) + '" /><input type="hidden" name="playerBaseUrl" value="' + escapeHtml(client.player_url || '') + '" /><button type="submit" class="' + pauseButtonClass + '" data-action="pause" aria-label="' + pauseButtonLabel + '" title="' + pauseButtonLabel + '"><i class="bi ' + pauseButtonIcon + ' me-1" aria-hidden="true"></i>' + pauseButtonText + '</button></form>',
|
|
'<form method="post" action="/clients/' + encodeURIComponent(client.screen_slug) + '/commands" class="d-inline-block m-0" data-async-command><input type="hidden" name="command" value="blackout" /><input type="hidden" name="blackout" value="' + (blackout ? 'false' : 'true') + '" /><input type="hidden" name="connectionId" value="' + escapeHtml(commandTargetId) + '" /><input type="hidden" name="playerBaseUrl" value="' + escapeHtml(client.player_url || '') + '" /><button type="submit" class="' + blackoutButtonClass + '" data-action="blackout" aria-label="' + blackoutButtonLabel + '" title="' + blackoutButtonLabel + '"><i class="bi ' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + blackoutButtonText + '</button></form>',
|
|
'</div>'
|
|
].join('');
|
|
}
|
|
|
|
function getClientMoveModalElements() {
|
|
return {
|
|
modal: document.getElementById('client-move-screen-modal'),
|
|
form: document.getElementById('client-move-screen-form'),
|
|
targetSelect: document.getElementById('client-move-screen-target'),
|
|
connectionInput: document.querySelector('[data-client-move-connection-id]'),
|
|
clientIdInput: document.querySelector('[data-client-move-client-id]'),
|
|
clientNameInput: document.querySelector('[data-client-move-client-name]'),
|
|
playerBaseUrlInput: document.querySelector('[data-client-move-player-base-url]')
|
|
};
|
|
}
|
|
|
|
function updateClientMoveModalFromRow(row) {
|
|
var elements = getClientMoveModalElements();
|
|
if (!elements.form || !elements.targetSelect || !row) {
|
|
return;
|
|
}
|
|
|
|
var currentScreenSlug = String(row.getAttribute('data-client-screen-slug') || '').trim();
|
|
var connectionId = String(row.getAttribute('data-client-client-id') || row.getAttribute('data-client-id') || '').trim();
|
|
var clientId = String(row.getAttribute('data-client-client-id') || '').trim();
|
|
var playerBaseUrl = String(row.getAttribute('data-client-player-base-url') || '').trim();
|
|
var clientNameCell = row.querySelector('td[data-label="Client"] > div');
|
|
var clientName = String(clientNameCell && clientNameCell.textContent || '').trim();
|
|
var options = Array.prototype.slice.call(elements.targetSelect.options || []);
|
|
|
|
options.forEach(function (option) {
|
|
if (!option) {
|
|
return;
|
|
}
|
|
option.disabled = String(option.value || '').trim() === currentScreenSlug;
|
|
});
|
|
|
|
elements.form.action = currentScreenSlug ? '/clients/' + encodeURIComponent(currentScreenSlug) + '/commands' : '#';
|
|
if (elements.connectionInput) {
|
|
elements.connectionInput.value = connectionId;
|
|
}
|
|
if (elements.clientIdInput) {
|
|
elements.clientIdInput.value = clientId;
|
|
}
|
|
if (elements.clientNameInput) {
|
|
elements.clientNameInput.value = clientName;
|
|
}
|
|
if (elements.playerBaseUrlInput) {
|
|
elements.playerBaseUrlInput.value = playerBaseUrl;
|
|
}
|
|
if (elements.targetSelect) {
|
|
elements.targetSelect.value = '';
|
|
}
|
|
if (elements.form.querySelector('button[type="submit"]')) {
|
|
elements.form.querySelector('button[type="submit"]').disabled = false;
|
|
}
|
|
}
|
|
|
|
function updateClientActionCell(cell, client) {
|
|
if (!cell) {
|
|
return;
|
|
}
|
|
|
|
var pauseButton = cell.querySelector('button[data-action="pause"]');
|
|
if (!pauseButton) {
|
|
cell.innerHTML = renderClientActionCell(client);
|
|
return;
|
|
}
|
|
|
|
var paused = Boolean(client.paused);
|
|
setButtonVariant(pauseButton, ['btn-secondary', 'btn-outline-primary'], 'btn-info');
|
|
pauseButton.setAttribute('aria-label', paused ? 'Resume client' : 'Pause client');
|
|
pauseButton.setAttribute('title', paused ? 'Resume client' : 'Pause client');
|
|
pauseButton.innerHTML = '<i class="bi ' + (paused ? 'bi-play-fill' : 'bi-pause-fill') + ' me-1" aria-hidden="true"></i>' + (paused ? 'Resume' : 'Pause');
|
|
|
|
var pauseForm = pauseButton.form;
|
|
if (pauseForm) {
|
|
var commandInput = pauseForm.querySelector('input[name="command"]');
|
|
if (commandInput) {
|
|
commandInput.value = 'pause';
|
|
}
|
|
var connectionInput = pauseForm.querySelector('input[name="connectionId"]');
|
|
if (connectionInput) {
|
|
connectionInput.value = client.clientId || client.id || '';
|
|
}
|
|
var playerBaseUrlInput = pauseForm.querySelector('input[name="playerBaseUrl"]');
|
|
if (playerBaseUrlInput) {
|
|
playerBaseUrlInput.value = client.player_url || '';
|
|
}
|
|
pauseForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
|
}
|
|
|
|
var reloadButton = cell.querySelector('button[data-action="reload"]');
|
|
if (reloadButton) {
|
|
reloadButton.setAttribute('aria-label', 'Reload client');
|
|
reloadButton.setAttribute('title', 'Reload client');
|
|
reloadButton.innerHTML = '<i class="bi bi-arrow-repeat" aria-hidden="true"></i>';
|
|
setButtonVariant(reloadButton, ['btn-danger', 'btn-success', 'btn-outline-secondary', 'btn-outline-dark', 'btn-outline-primary', 'btn-secondary'], 'btn-danger');
|
|
var reloadForm = reloadButton.form;
|
|
if (reloadForm) {
|
|
var reloadInput = reloadForm.querySelector('input[name="connectionId"]');
|
|
if (reloadInput) {
|
|
reloadInput.value = client.clientId || client.id || '';
|
|
}
|
|
var reloadPlayerBaseUrlInput = reloadForm.querySelector('input[name="playerBaseUrl"]');
|
|
if (reloadPlayerBaseUrlInput) {
|
|
reloadPlayerBaseUrlInput.value = client.player_url || '';
|
|
}
|
|
reloadForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
|
reloadForm.setAttribute('data-confirm-message', 'Reloading will restart the player page. Continue?');
|
|
}
|
|
}
|
|
|
|
var blackoutButton = cell.querySelector('button[data-action="blackout"]');
|
|
if (!blackoutButton) {
|
|
cell.innerHTML = renderClientActionCell(client);
|
|
return;
|
|
}
|
|
|
|
var blackout = Boolean(client.blackout);
|
|
var blackoutButtonIcon = blackout ? 'bi-eye' : 'bi-eye-slash';
|
|
setButtonVariant(blackoutButton, ['btn-success', 'btn-secondary'], blackout ? 'btn-success' : 'btn-secondary');
|
|
blackoutButton.setAttribute('aria-label', blackout ? 'Restore client' : 'Blackout client');
|
|
blackoutButton.setAttribute('title', blackout ? 'Restore client' : 'Blackout client');
|
|
blackoutButton.innerHTML = '<i class="bi ' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + (blackout ? 'Restore' : 'Blackout');
|
|
|
|
var blackoutForm = blackoutButton.form;
|
|
if (blackoutForm) {
|
|
var blackoutCommandInput = blackoutForm.querySelector('input[name="command"]');
|
|
if (blackoutCommandInput) {
|
|
blackoutCommandInput.value = 'blackout';
|
|
}
|
|
var blackoutStateInput = blackoutForm.querySelector('input[name="blackout"]');
|
|
if (blackoutStateInput) {
|
|
blackoutStateInput.value = blackout ? 'false' : 'true';
|
|
}
|
|
var blackoutConnectionInput = blackoutForm.querySelector('input[name="connectionId"]');
|
|
if (blackoutConnectionInput) {
|
|
blackoutConnectionInput.value = client.clientId || client.id || '';
|
|
}
|
|
var blackoutPlayerBaseUrlInput = blackoutForm.querySelector('input[name="playerBaseUrl"]');
|
|
if (blackoutPlayerBaseUrlInput) {
|
|
blackoutPlayerBaseUrlInput.value = client.player_url || '';
|
|
}
|
|
blackoutForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
|
}
|
|
|
|
var previousButton = cell.querySelector('button[data-action="previous"]');
|
|
if (!previousButton) {
|
|
cell.innerHTML = renderClientActionCell(client);
|
|
return;
|
|
}
|
|
setButtonVariant(previousButton, ['btn-outline-secondary', 'btn-success', 'btn-danger', 'btn-primary', 'btn-secondary', 'btn-warning'], 'btn-warning');
|
|
previousButton.setAttribute('aria-label', 'Previous slide');
|
|
previousButton.setAttribute('title', 'Previous slide');
|
|
var previousForm = previousButton.form;
|
|
if (previousForm) {
|
|
var previousCommandInput = previousForm.querySelector('input[name="command"]');
|
|
if (previousCommandInput) {
|
|
previousCommandInput.value = 'previous';
|
|
}
|
|
var previousConnectionInput = previousForm.querySelector('input[name="connectionId"]');
|
|
if (previousConnectionInput) {
|
|
previousConnectionInput.value = client.clientId || client.id || '';
|
|
}
|
|
var previousPlayerBaseUrlInput = previousForm.querySelector('input[name="playerBaseUrl"]');
|
|
if (previousPlayerBaseUrlInput) {
|
|
previousPlayerBaseUrlInput.value = client.player_url || '';
|
|
}
|
|
previousForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
|
}
|
|
|
|
var nextButton = cell.querySelector('button[data-action="next"]');
|
|
if (!nextButton) {
|
|
cell.innerHTML = renderClientActionCell(client);
|
|
return;
|
|
}
|
|
setButtonVariant(nextButton, ['btn-outline-secondary', 'btn-success', 'btn-danger', 'btn-primary', 'btn-secondary', 'btn-warning'], 'btn-warning');
|
|
nextButton.setAttribute('aria-label', 'Next slide');
|
|
nextButton.setAttribute('title', 'Next slide');
|
|
var nextForm = nextButton.form;
|
|
if (nextForm) {
|
|
var nextCommandInput = nextForm.querySelector('input[name="command"]');
|
|
if (nextCommandInput) {
|
|
nextCommandInput.value = 'next';
|
|
}
|
|
var nextConnectionInput = nextForm.querySelector('input[name="connectionId"]');
|
|
if (nextConnectionInput) {
|
|
nextConnectionInput.value = client.clientId || client.id || '';
|
|
}
|
|
var nextPlayerBaseUrlInput = nextForm.querySelector('input[name="playerBaseUrl"]');
|
|
if (nextPlayerBaseUrlInput) {
|
|
nextPlayerBaseUrlInput.value = client.player_url || '';
|
|
}
|
|
nextForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
|
}
|
|
}
|
|
|
|
function syncClientActionCell(row, client, hasActionsColumn) {
|
|
if (!row || !row.cells) {
|
|
return;
|
|
}
|
|
|
|
if (!hasActionsColumn) {
|
|
if (row.cells.length > 5) {
|
|
row.deleteCell(row.cells.length - 1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
var actionCell = row.cells.length > 5 ? row.cells[5] : null;
|
|
if (!actionCell) {
|
|
actionCell = row.insertCell(-1);
|
|
actionCell.setAttribute('data-label', 'Actions');
|
|
actionCell.className = 'text-end';
|
|
}
|
|
|
|
updateClientActionCell(actionCell, client);
|
|
}
|
|
|
|
function renderClientRow(client, hasActionsColumn) {
|
|
var connectedAt = client.connectedAt ? '<div>' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '</div>' + (client.lastSeenAt ? '<div class="subtle connected-updated-secondary">' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '</div>' : '') : '<span class="empty">Unknown</span>';
|
|
var viewport = client.viewport && client.viewport.width && client.viewport.height ? escapeHtml(client.viewport.width + 'x' + client.viewport.height) : '<span class="empty">Unknown</span>';
|
|
var clientNameValue = getClientDisplayName(client);
|
|
var clientName = clientNameValue ? escapeHtml(clientNameValue) : '<span class="empty">Unknown</span>';
|
|
var screenName = client.screen_name ? escapeHtml(client.screen_name) : '<span class="empty">Unknown</span>';
|
|
var currentSlide = client.currentSlideTitle ? escapeHtml(client.currentSlideTitle) : '<span class="empty">No slide currently showing</span>';
|
|
var actionCell = hasActionsColumn ? '<td data-label="Actions">' + renderClientActionCell(client) + '</td>' : '';
|
|
|
|
return [
|
|
'<tr data-client-key="' + escapeHtml(getClientRowKey(client)) + '" data-client-id="' + escapeHtml(client.id || '') + '" data-client-client-id="' + escapeHtml(client.clientId || '') + '" data-client-device-id="' + escapeHtml(client.deviceId || '') + '" data-client-screen-slug="' + escapeHtml(client.screen_slug || '') + '" data-client-player-base-url="' + escapeHtml(client.player_url || '') + '">',
|
|
'<td data-label="Client"><div>' + clientName + '</div></td>',
|
|
'<td data-label="Current Screen"><div>' + screenName + '</div></td>',
|
|
'<td data-label="Current Slide">' + currentSlide + '</td>',
|
|
'<td data-label="Viewport">' + viewport + '</td>',
|
|
'<td data-label="Connected/Updated">' + connectedAt + '</td>',
|
|
actionCell,
|
|
'</tr>'
|
|
].join('');
|
|
}
|
|
|
|
function updateClientRowCells(row, client, hasActionsColumn) {
|
|
if (!row || !row.cells || row.cells.length < 5) {
|
|
return;
|
|
}
|
|
|
|
var connectedAt = client.connectedAt ? '<div>' + escapeHtml(client.connectedAtLabel || formatDashboardDate(client.connectedAt) || client.connectedAt) + '</div>' + (client.lastSeenAt ? '<div class="subtle connected-updated-secondary">' + escapeHtml(client.lastSeenAtLabel || formatDashboardDate(client.lastSeenAt) || client.lastSeenAt) + '</div>' : '') : '<span class="empty">Unknown</span>';
|
|
var viewport = client.viewport && client.viewport.width && client.viewport.height ? escapeHtml(client.viewport.width + 'x' + client.viewport.height) : '<span class="empty">Unknown</span>';
|
|
var clientNameValue = getClientDisplayName(client);
|
|
var clientName = clientNameValue ? escapeHtml(clientNameValue) : '<span class="empty">Unknown</span>';
|
|
var screenName = client.screen_name ? escapeHtml(client.screen_name) : '<span class="empty">Unknown</span>';
|
|
var currentSlide = client.currentSlideTitle ? escapeHtml(client.currentSlideTitle) : '<span class="empty">No slide currently showing</span>';
|
|
|
|
row.setAttribute('data-client-key', escapeHtml(getClientRowKey(client)));
|
|
row.setAttribute('data-client-id', escapeHtml(client.id || ''));
|
|
row.setAttribute('data-client-client-id', escapeHtml(client.clientId || ''));
|
|
row.setAttribute('data-client-device-id', escapeHtml(client.deviceId || ''));
|
|
row.setAttribute('data-client-screen-slug', escapeHtml(client.screen_slug || ''));
|
|
row.setAttribute('data-client-player-base-url', escapeHtml(client.player_url || ''));
|
|
|
|
setCellHtml(row.cells[0], '<div>' + clientName + '</div>');
|
|
setCellHtml(row.cells[1], '<div>' + screenName + '</div>');
|
|
setCellHtml(row.cells[2], currentSlide);
|
|
setCellHtml(row.cells[3], viewport);
|
|
setCellHtml(row.cells[4], connectedAt);
|
|
|
|
syncClientActionCell(row, client, hasActionsColumn);
|
|
}
|
|
|
|
function createClientRowFromTemplate(client, hasActionsColumn) {
|
|
var template = document.createElement('tbody');
|
|
template.innerHTML = renderClientRow(client, hasActionsColumn);
|
|
return template.firstElementChild || null;
|
|
}
|
|
|
|
function setCellHtml(cell, html) {
|
|
if (!cell || String(cell.innerHTML || '') === String(html || '')) {
|
|
return false;
|
|
}
|
|
|
|
cell.innerHTML = html;
|
|
return true;
|
|
}
|
|
|
|
function renderScreenTile(screen) {
|
|
var clientCount = Number(screen.player_connection_count || 0);
|
|
var connectionLabel = clientCount ? clientCount + ' live' : 'No clients';
|
|
var connectionStateClass = clientCount ? 'is-live' : 'is-idle';
|
|
var playlistLabel = screen.playlist_name ? escapeHtml(screen.playlist_name) : 'Unassigned';
|
|
|
|
return [
|
|
'<article class="dashboard-screen-tile" data-screen-key="' + escapeHtml(screen.id || '') + '">',
|
|
'<div class="dashboard-screen-tile-top">',
|
|
'<div class="dashboard-screen-tile-text">',
|
|
'<h4 class="dashboard-screen-name">' + escapeHtml(screen.name || '') + '</h4>',
|
|
'</div>',
|
|
'<span class="dashboard-screen-pill ' + connectionStateClass + '">' + escapeHtml(connectionLabel) + '</span>',
|
|
'</div>',
|
|
'<dl class="dashboard-screen-meta">',
|
|
'<div><dt>Playlist</dt><dd>' + playlistLabel + '</dd></div>',
|
|
'</dl>',
|
|
'</article>'
|
|
].join('');
|
|
}
|
|
|
|
function compareScreensByConnectedClients(left, right) {
|
|
var leftCount = Number(left && left.player_connection_count || 0);
|
|
var rightCount = Number(right && right.player_connection_count || 0);
|
|
|
|
if (leftCount !== rightCount) {
|
|
return rightCount - leftCount;
|
|
}
|
|
|
|
var leftName = String(left && left.name || '').trim();
|
|
var rightName = String(right && right.name || '').trim();
|
|
var nameCompare = leftName.localeCompare(rightName, undefined, { sensitivity: 'base', numeric: true });
|
|
|
|
if (nameCompare !== 0) {
|
|
return nameCompare;
|
|
}
|
|
|
|
return String(left && left.slug || '').localeCompare(String(right && right.slug || ''), undefined, { sensitivity: 'base', numeric: true });
|
|
}
|
|
|
|
function updateStats(state) {
|
|
var playerCount = document.getElementById('dashboard-player-count');
|
|
var screenCount = document.getElementById('dashboard-screen-count');
|
|
var slideCount = document.getElementById('dashboard-slide-count');
|
|
var playlistCount = document.getElementById('dashboard-playlist-count');
|
|
|
|
if (playlistCount && Array.isArray(state.playlists)) {
|
|
playlistCount.textContent = String(state.playlists.length);
|
|
}
|
|
if (slideCount && Array.isArray(state.slides)) {
|
|
slideCount.textContent = String(state.slides.length);
|
|
}
|
|
if (screenCount && Array.isArray(state.screens)) {
|
|
screenCount.textContent = String(state.screens.length);
|
|
}
|
|
if (playerCount) {
|
|
playerCount.textContent = String(Number(state.connectedPlayersCount || 0));
|
|
}
|
|
}
|
|
|
|
function updateClientTable(state, forceRender) {
|
|
var tbody = document.getElementById('dashboard-clients-table-body');
|
|
if (!tbody || !Array.isArray(state.clients)) {
|
|
return;
|
|
}
|
|
if (!forceRender && isClientSearchLoading()) {
|
|
return;
|
|
}
|
|
var table = document.getElementById('dashboard-clients-table');
|
|
var hasActionsColumn = Boolean(table && String(table.getAttribute('data-has-actions-column') || '').toLowerCase() === 'true');
|
|
var visibleClients = getVisibleClients(state);
|
|
|
|
var canPatchRows = typeof tbody.querySelectorAll === 'function'
|
|
&& typeof tbody.insertBefore === 'function'
|
|
&& typeof tbody.removeChild === 'function'
|
|
&& typeof document.createElement === 'function';
|
|
|
|
if (!canPatchRows) {
|
|
tbody.innerHTML = visibleClients.map(function (client) {
|
|
return renderClientRow(client, hasActionsColumn);
|
|
}).join('');
|
|
return;
|
|
}
|
|
|
|
var existingRows = {};
|
|
Array.prototype.slice.call(tbody.querySelectorAll('tr[data-client-key]')).forEach(function (row) {
|
|
existingRows[String(row.getAttribute('data-client-key') || '').trim()] = row;
|
|
});
|
|
|
|
var visibleRowKeys = {};
|
|
visibleClients.forEach(function (client) {
|
|
visibleRowKeys[String(getClientRowKey(client) || '').trim()] = true;
|
|
});
|
|
|
|
function getClientIdentity(client) {
|
|
return [
|
|
String(client && (client.client_name || client.name || '') || '').trim().toLowerCase(),
|
|
String(client && (client.player_url || client.playerPublicBaseUrl || '') || '').trim().replace(/\/$/, '').toLowerCase()
|
|
].join('|');
|
|
}
|
|
|
|
var visibleClientIdentities = {};
|
|
visibleClients.forEach(function (client) {
|
|
var identity = getClientIdentity(client);
|
|
if (identity !== '|') {
|
|
visibleClientIdentities[identity] = true;
|
|
}
|
|
});
|
|
|
|
var nextRows = visibleClients.map(function (client) {
|
|
var rowKey = String(getClientRowKey(client) || '').trim();
|
|
if (pendingClientRowRemovals[rowKey]) {
|
|
clearTimeout(pendingClientRowRemovals[rowKey]);
|
|
delete pendingClientRowRemovals[rowKey];
|
|
}
|
|
var row = existingRows[rowKey] || null;
|
|
|
|
if (!row) {
|
|
row = createClientRowFromTemplate(client, hasActionsColumn);
|
|
} else {
|
|
updateClientRowCells(row, client, hasActionsColumn);
|
|
}
|
|
|
|
return row;
|
|
}).filter(function (row) {
|
|
return Boolean(row);
|
|
});
|
|
|
|
Object.keys(existingRows).forEach(function (rowKey) {
|
|
if (visibleRowKeys[rowKey]) {
|
|
return;
|
|
}
|
|
|
|
var existingRow = existingRows[rowKey];
|
|
var existingNameCell = existingRow.querySelector('td[data-label="Client"] > div');
|
|
var existingIdentity = [
|
|
String(existingNameCell && existingNameCell.textContent || '').trim().toLowerCase(),
|
|
String(existingRow.getAttribute('data-client-player-base-url') || '').trim().replace(/\/$/, '').toLowerCase()
|
|
].join('|');
|
|
if (existingIdentity !== '|' && visibleClientIdentities[existingIdentity]) {
|
|
if (pendingClientRowRemovals[rowKey]) {
|
|
clearTimeout(pendingClientRowRemovals[rowKey]);
|
|
delete pendingClientRowRemovals[rowKey];
|
|
}
|
|
if (existingRow.parentNode === tbody) {
|
|
tbody.removeChild(existingRow);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (pendingClientRowRemovals[rowKey]) {
|
|
return;
|
|
}
|
|
|
|
pendingClientRowRemovals[rowKey] = setTimeout(function () {
|
|
delete pendingClientRowRemovals[rowKey];
|
|
var row = existingRows[rowKey];
|
|
if (row && row.parentNode === tbody && !visibleRowKeys[rowKey]) {
|
|
tbody.removeChild(row);
|
|
}
|
|
if (!tbody.querySelector('tr[data-client-key]')) {
|
|
tbody.innerHTML = '<tr><td colspan="' + (hasActionsColumn ? '7' : '6') + '" class="empty">No connected clients yet.</td></tr>';
|
|
}
|
|
}, CLIENT_ROW_REMOVAL_DELAY_MS);
|
|
});
|
|
|
|
nextRows.forEach(function (row, index) {
|
|
var referenceNode = tbody.children[index] || null;
|
|
if (referenceNode !== row) {
|
|
tbody.insertBefore(row, referenceNode);
|
|
}
|
|
});
|
|
|
|
if (!visibleClients.length && !tbody.querySelector('tr[data-client-key]')) {
|
|
tbody.innerHTML = '<tr><td colspan="' + (hasActionsColumn ? '7' : '6') + '" class="empty">No connected clients yet.</td></tr>';
|
|
}
|
|
|
|
}
|
|
|
|
function refreshClientTableFromLatestState() {
|
|
if (!latestDashboardState) {
|
|
return;
|
|
}
|
|
|
|
updateClientTable(latestDashboardState, true);
|
|
}
|
|
|
|
function initClientMoveHandler() {
|
|
var elements = getClientMoveModalElements();
|
|
if (!elements.modal || !elements.form || !elements.targetSelect) {
|
|
return;
|
|
}
|
|
|
|
if (typeof elements.modal.addEventListener === 'function') {
|
|
elements.modal.addEventListener('show.bs.modal', function (event) {
|
|
var trigger = event && event.relatedTarget ? event.relatedTarget : null;
|
|
var row = trigger && trigger.closest ? trigger.closest('tr[data-client-key]') : null;
|
|
if (row) {
|
|
updateClientMoveModalFromRow(row);
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener('click', function (event) {
|
|
var moveButton = event.target && event.target.closest ? event.target.closest('button[data-action="move-screen"]') : null;
|
|
if (!moveButton) {
|
|
return;
|
|
}
|
|
|
|
if (event && typeof event.preventDefault === 'function') {
|
|
event.preventDefault();
|
|
}
|
|
|
|
var row = moveButton.closest ? moveButton.closest('tr[data-client-key]') : null;
|
|
if (!row) {
|
|
return;
|
|
}
|
|
|
|
updateClientMoveModalFromRow(row);
|
|
|
|
if (window.pulseModal && typeof window.pulseModal.show === 'function') {
|
|
window.pulseModal.show(elements.modal);
|
|
return;
|
|
}
|
|
|
|
if (window.bootstrap && window.bootstrap.Modal) {
|
|
window.bootstrap.Modal.getOrCreateInstance(elements.modal).show();
|
|
}
|
|
});
|
|
|
|
elements.form.addEventListener('submit', function (event) {
|
|
if (elements.form.dataset && elements.form.dataset.busy === 'true') {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
var targetScreenSlug = String(elements.targetSelect.value || '').trim();
|
|
if (!targetScreenSlug) {
|
|
event.preventDefault();
|
|
window.alert('Choose a target screen.');
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
elements.form.dataset.busy = 'true';
|
|
|
|
var formData = new FormData(elements.form);
|
|
var body = new URLSearchParams();
|
|
formData.forEach(function (value, key) {
|
|
body.append(key, value);
|
|
});
|
|
|
|
fetch(elements.form.action, {
|
|
method: (elements.form.method || 'POST').toUpperCase(),
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/json, text/plain, */*'
|
|
},
|
|
body: body.toString(),
|
|
credentials: 'same-origin'
|
|
}).then(function (response) {
|
|
if (!response.ok) {
|
|
return response.text().then(function (text) {
|
|
var error = new Error(text || 'Unable to move client.');
|
|
try {
|
|
var payload = JSON.parse(text);
|
|
if (payload && (payload.error || payload.message)) {
|
|
error = new Error(String(payload.error || payload.message));
|
|
}
|
|
} catch (_error) {
|
|
// fall back to the raw text body
|
|
}
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
if (window.pulseModal && typeof window.pulseModal.hide === 'function') {
|
|
window.pulseModal.hide(elements.modal);
|
|
} else if (window.bootstrap && window.bootstrap.Modal) {
|
|
window.bootstrap.Modal.getOrCreateInstance(elements.modal).hide();
|
|
}
|
|
return response.json().catch(function () {
|
|
return { ok: true };
|
|
});
|
|
}).catch(function (error) {
|
|
window.alert(error && error.message ? error.message : 'Unable to move client.');
|
|
}).finally(function () {
|
|
delete elements.form.dataset.busy;
|
|
});
|
|
});
|
|
}
|
|
|
|
function updateKioskLauncherModal(state) {
|
|
var modal = document.getElementById('dashboard-kiosk-launcher-modal');
|
|
if (!modal) {
|
|
return;
|
|
}
|
|
|
|
var checkbox = modal.querySelector('[data-kiosk-launcher-confirm]');
|
|
var select = modal.querySelector('[data-kiosk-launcher-player-select]');
|
|
var downloadLinks = Array.prototype.slice.call(modal.querySelectorAll('[data-kiosk-launcher-download]'));
|
|
var playersState = state && Array.isArray(state.kioskPlayers)
|
|
? state.kioskPlayers
|
|
: (state && Array.isArray(state.clients) ? state.clients : []);
|
|
var players = playersState.filter(function (client) {
|
|
return Boolean(client && String(client.player_url || '').trim());
|
|
});
|
|
|
|
if (select && playersState.length) {
|
|
var currentValue = String(select.value || '').trim();
|
|
var options = ['<option value="">Select a player</option>'];
|
|
|
|
players.forEach(function (player) {
|
|
var playerUrl = String(player.player_url || '').trim();
|
|
var playerIdentifier = String(player.player_identifier || 'Connected player').trim();
|
|
if (!playerUrl) {
|
|
return;
|
|
}
|
|
options.push('<option value="' + escapeHtml(playerUrl) + '" data-player-identifier="' + escapeHtml(playerIdentifier) + '">' + escapeHtml(playerIdentifier + ' - ' + playerUrl) + '</option>');
|
|
});
|
|
|
|
select.innerHTML = options.join('');
|
|
if (currentValue) {
|
|
select.value = currentValue;
|
|
}
|
|
}
|
|
|
|
var selectedUrl = select ? String(select.value || '').trim() : '';
|
|
var canEnableDownloads = Boolean(checkbox && checkbox.checked && selectedUrl);
|
|
downloadLinks.forEach(function (link) {
|
|
if (!link) {
|
|
return;
|
|
}
|
|
var baseHref = String(link.getAttribute('data-kiosk-launcher-download-base') || link.getAttribute('href') || '').trim();
|
|
if (canEnableDownloads) {
|
|
link.setAttribute('href', baseHref + '?playerUrl=' + encodeURIComponent(selectedUrl));
|
|
link.classList.remove('disabled');
|
|
link.setAttribute('aria-disabled', 'false');
|
|
link.removeAttribute('tabindex');
|
|
} else {
|
|
link.removeAttribute('href');
|
|
link.classList.add('disabled');
|
|
link.setAttribute('aria-disabled', 'true');
|
|
link.setAttribute('tabindex', '-1');
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateScreenGrid(state) {
|
|
var grid = document.getElementById('dashboard-screens-grid');
|
|
if (!grid || !Array.isArray(state.screens)) {
|
|
return;
|
|
}
|
|
var screens = state.screens.slice().sort(compareScreensByConnectedClients);
|
|
|
|
if (!screens.length) {
|
|
grid.innerHTML = '<div class="empty dashboard-screen-empty">No screens yet.</div>';
|
|
return;
|
|
}
|
|
grid.innerHTML = screens.map(renderScreenTile).join('');
|
|
}
|
|
function updateDashboardQuickActions(state) {
|
|
var pauseButton = document.getElementById('dashboard-pause-all-button');
|
|
var blackoutButton = document.getElementById('dashboard-blackout-all-button');
|
|
if ((!pauseButton && !blackoutButton) || !state || !Array.isArray(state.clients)) {
|
|
return;
|
|
}
|
|
|
|
var hasClients = state.clients.length > 0;
|
|
var allPaused = hasClients && state.clients.every(function (client) {
|
|
return Boolean(client && client.paused);
|
|
});
|
|
var allBlackout = hasClients && state.clients.every(function (client) {
|
|
return Boolean(client && client.blackout);
|
|
});
|
|
if (pauseButton) {
|
|
var pauseForm = pauseButton.form;
|
|
var pauseInput = pauseForm ? pauseForm.querySelector('input[name="paused"]') : null;
|
|
var pauseLabel = allPaused ? 'Resume all clients' : 'Pause all clients';
|
|
var pauseButtonIcon = allPaused ? 'bi-play-fill' : 'bi-pause-fill';
|
|
pauseButton.innerHTML = '<i class="bi ' + pauseButtonIcon + ' me-1" aria-hidden="true"></i>' + escapeHtml(pauseLabel);
|
|
setButtonVariant(pauseButton, ['btn-success', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], allPaused ? 'btn-success' : 'btn-info');
|
|
if (pauseInput) {
|
|
pauseInput.value = allPaused ? 'false' : 'true';
|
|
}
|
|
if (pauseForm) {
|
|
pauseForm.setAttribute('data-confirm-message', allPaused ? 'Resume all connected clients?' : 'Pause all connected clients?');
|
|
}
|
|
pauseButton.setAttribute('aria-label', pauseLabel);
|
|
}
|
|
|
|
var label = allBlackout ? 'Restore all clients' : 'Blackout all clients';
|
|
var blackoutForm = blackoutButton.form;
|
|
var blackoutInput = blackoutForm ? blackoutForm.querySelector('input[name="blackout"]') : null;
|
|
var blackoutButtonIcon = allBlackout ? 'bi-eye' : 'bi-eye-slash';
|
|
|
|
blackoutButton.innerHTML = '<i class="bi ' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + escapeHtml(label);
|
|
setButtonVariant(blackoutButton, ['btn-success', 'btn-danger', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], allBlackout ? 'btn-success' : 'btn-secondary');
|
|
if (blackoutInput) {
|
|
blackoutInput.value = allBlackout ? 'false' : 'true';
|
|
}
|
|
if (blackoutForm) {
|
|
blackoutForm.setAttribute('data-confirm-message', allBlackout ? 'Restore all connected clients?' : 'Blackout all connected clients?');
|
|
}
|
|
blackoutButton.setAttribute('aria-label', label);
|
|
}
|
|
|
|
function handleDashboardState(state) {
|
|
if (!state) {
|
|
return;
|
|
}
|
|
latestDashboardState = state;
|
|
window.webLatestDashboardState = latestDashboardState;
|
|
updateStats(state);
|
|
updateScreenGrid(state);
|
|
updateClientTable(state);
|
|
updateKioskLauncherModal(state);
|
|
updateDashboardQuickActions(state);
|
|
updateScreenCommandControls();
|
|
}
|
|
|
|
function sendClientRename(screenSlug, connectionId, clientId, deviceId, clientName) {
|
|
var body = new URLSearchParams();
|
|
body.append('command', 'setClientName');
|
|
body.append('connectionId', String(connectionId || '').trim());
|
|
body.append('clientId', String(clientId || '').trim());
|
|
body.append('deviceId', String(deviceId || '').trim());
|
|
body.append('clientName', String(clientName || '').trim());
|
|
|
|
return fetch('/clients/' + encodeURIComponent(String(screenSlug || '').trim()) + '/commands', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/json, text/plain, */*'
|
|
},
|
|
body: body.toString(),
|
|
credentials: 'same-origin'
|
|
}).then(function (response) {
|
|
if (!response.ok) {
|
|
return response.text().then(function (text) {
|
|
var message = text || 'Unable to rename client.';
|
|
try {
|
|
var payload = JSON.parse(text);
|
|
message = payload && (payload.error || payload.message) ? String(payload.error || payload.message) : message;
|
|
} catch (_error) {
|
|
// fall back to the raw text body
|
|
}
|
|
throw new Error(message);
|
|
});
|
|
}
|
|
return response.json().catch(function () {
|
|
return { ok: true };
|
|
});
|
|
});
|
|
}
|
|
|
|
function initClientRenameHandler() {
|
|
var table = document.getElementById('dashboard-clients-table');
|
|
var tbody = document.getElementById('dashboard-clients-table-body');
|
|
if (!table || !tbody) {
|
|
return;
|
|
}
|
|
|
|
tbody.addEventListener('dblclick', function (event) {
|
|
var cell = event.target && event.target.closest ? event.target.closest('td[data-label="Client"]') : null;
|
|
if (!cell || !tbody.contains(cell)) {
|
|
return;
|
|
}
|
|
|
|
var row = cell.parentElement;
|
|
if (!row) {
|
|
return;
|
|
}
|
|
|
|
var connectionId = String(row.getAttribute('data-client-id') || '').trim();
|
|
var clientId = String(row.getAttribute('data-client-client-id') || '').trim();
|
|
var deviceId = String(row.getAttribute('data-client-device-id') || '').trim();
|
|
var screenSlug = String(row.getAttribute('data-client-screen-slug') || '').trim();
|
|
var currentName = String(cell.textContent || '').trim();
|
|
var nextName = window.prompt('Rename connected client', currentName && currentName !== 'Unknown' ? currentName : '');
|
|
if (nextName === null) {
|
|
return;
|
|
}
|
|
|
|
nextName = String(nextName || '').trim();
|
|
if (!nextName) {
|
|
window.alert('Client name is required.');
|
|
return;
|
|
}
|
|
if (!connectionId || !screenSlug) {
|
|
window.alert('Unable to rename this client right now.');
|
|
return;
|
|
}
|
|
|
|
sendClientRename(screenSlug, connectionId, clientId, deviceId, nextName).then(function () {
|
|
if (row && row.cells && row.cells[0]) {
|
|
row.cells[0].innerHTML = '<div>' + escapeHtml(nextName) + '</div>';
|
|
}
|
|
}).catch(function (error) {
|
|
window.alert(error && error.message ? error.message : 'Unable to rename client.');
|
|
});
|
|
});
|
|
}
|
|
|
|
function initClientMoveHandler() {
|
|
var elements = getClientMoveModalElements();
|
|
if (!elements.modal || !elements.form || !elements.targetSelect) {
|
|
return;
|
|
}
|
|
|
|
if (typeof elements.modal.addEventListener === 'function') {
|
|
elements.modal.addEventListener('show.bs.modal', function (event) {
|
|
var trigger = event && event.relatedTarget ? event.relatedTarget : null;
|
|
var row = trigger && trigger.closest ? trigger.closest('tr[data-client-key]') : null;
|
|
if (row) {
|
|
updateClientMoveModalFromRow(row);
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener('click', function (event) {
|
|
var moveButton = event.target && event.target.closest ? event.target.closest('button[data-action="move-screen"]') : null;
|
|
if (!moveButton) {
|
|
return;
|
|
}
|
|
|
|
if (event && typeof event.preventDefault === 'function') {
|
|
event.preventDefault();
|
|
}
|
|
|
|
var row = moveButton.closest ? moveButton.closest('tr[data-client-key]') : null;
|
|
if (!row) {
|
|
return;
|
|
}
|
|
|
|
updateClientMoveModalFromRow(row);
|
|
|
|
if (window.pulseModal && typeof window.pulseModal.show === 'function') {
|
|
window.pulseModal.show(elements.modal);
|
|
return;
|
|
}
|
|
|
|
if (window.bootstrap && window.bootstrap.Modal) {
|
|
window.bootstrap.Modal.getOrCreateInstance(elements.modal).show();
|
|
}
|
|
});
|
|
|
|
elements.form.addEventListener('submit', function (event) {
|
|
if (elements.form.dataset && elements.form.dataset.busy === 'true') {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
var targetScreenSlug = String(elements.targetSelect.value || '').trim();
|
|
if (!targetScreenSlug) {
|
|
event.preventDefault();
|
|
window.alert('Choose a target screen.');
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
elements.form.dataset.busy = 'true';
|
|
|
|
var formData = new FormData(elements.form);
|
|
var body = new URLSearchParams();
|
|
formData.forEach(function (value, key) {
|
|
body.append(key, value);
|
|
});
|
|
|
|
fetch(elements.form.action, {
|
|
method: (elements.form.method || 'POST').toUpperCase(),
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'Accept': 'application/json, text/plain, */*'
|
|
},
|
|
body: body.toString(),
|
|
credentials: 'same-origin'
|
|
}).then(function (response) {
|
|
if (!response.ok) {
|
|
return response.text().then(function (text) {
|
|
var error = new Error(text || 'Unable to move client.');
|
|
try {
|
|
var payload = JSON.parse(text);
|
|
if (payload && (payload.error || payload.message)) {
|
|
error = new Error(String(payload.error || payload.message));
|
|
}
|
|
} catch (_error) {
|
|
// fall back to the raw text body
|
|
}
|
|
throw error;
|
|
});
|
|
}
|
|
|
|
if (window.pulseModal && typeof window.pulseModal.hide === 'function') {
|
|
window.pulseModal.hide(elements.modal);
|
|
} else if (window.bootstrap && window.bootstrap.Modal) {
|
|
window.bootstrap.Modal.getOrCreateInstance(elements.modal).hide();
|
|
}
|
|
return response.json().catch(function () {
|
|
return { ok: true };
|
|
});
|
|
}).catch(function (error) {
|
|
window.alert(error && error.message ? error.message : 'Unable to move client.');
|
|
}).finally(function () {
|
|
delete elements.form.dataset.busy;
|
|
});
|
|
});
|
|
}
|
|
|
|
function initKioskLauncherModal() {
|
|
var modal = document.getElementById('dashboard-kiosk-launcher-modal');
|
|
if (!modal) {
|
|
return;
|
|
}
|
|
|
|
var checkbox = modal.querySelector('[data-kiosk-launcher-confirm]');
|
|
var select = modal.querySelector('[data-kiosk-launcher-player-select]');
|
|
var downloadLinks = Array.prototype.slice.call(modal.querySelectorAll('[data-kiosk-launcher-download]'));
|
|
|
|
function resetModalState() {
|
|
if (checkbox) {
|
|
checkbox.checked = false;
|
|
}
|
|
if (select) {
|
|
select.value = '';
|
|
}
|
|
updateKioskLauncherModal(latestDashboardState);
|
|
}
|
|
|
|
if (checkbox) {
|
|
checkbox.addEventListener('change', function () {
|
|
updateKioskLauncherModal(latestDashboardState || readScreenCommandStateFromDom());
|
|
});
|
|
}
|
|
|
|
if (select) {
|
|
select.addEventListener('change', function () {
|
|
updateKioskLauncherModal(latestDashboardState || readScreenCommandStateFromDom());
|
|
});
|
|
}
|
|
|
|
modal.addEventListener('show.bs.modal', resetModalState);
|
|
modal.addEventListener('hidden.bs.modal', resetModalState);
|
|
modal.addEventListener('click', function (event) {
|
|
var target = event.target && event.target.closest ? event.target.closest('[data-kiosk-launcher-download]') : null;
|
|
if (target && target.classList.contains('disabled')) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
});
|
|
|
|
resetModalState();
|
|
}
|
|
|
|
window.webHandleDashboardState = handleDashboardState;
|
|
window.webRefreshClientTableFromLatestState = refreshClientTableFromLatestState;
|
|
|
|
initClientRenameHandler();
|
|
initClientMoveHandler();
|
|
initKioskLauncherModal();
|
|
initScreenCommandControls();
|
|
}());
|