427 lines
21 KiB
JavaScript
427 lines
21 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;
|
|
|
|
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' : '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' : 'Blackout';
|
|
var reloadConfirmMessage = 'Reloading will restart the player page. Continue?';
|
|
var blackoutCommandValue = blackout ? 'false' : 'true';
|
|
|
|
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(client.id) + '" /><button type="submit" class="btn btn-sm btn-danger" data-action="reload"><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload</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="previous" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="btn btn-sm btn-outline-secondary" data-action="previous" aria-label="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(client.id) + '" /><button type="submit" class="btn btn-sm btn-outline-secondary" data-action="next" aria-label="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(client.id) + '" /><button type="submit" class="' + pauseButtonClass + '" data-action="pause"><i class="bi bi-' + pauseButtonIcon + ' me-1" aria-hidden="true"></i>' + pauseButtonLabel + '</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="' + blackoutCommandValue + '" /><input type="hidden" name="connectionId" value="' + escapeHtml(client.id) + '" /><button type="submit" class="' + blackoutButtonClass + '" data-action="blackout"><i class="bi bi-' + blackoutButtonIcon + ' me-1" aria-hidden="true"></i>' + blackoutButtonLabel + '</button></form></div>';
|
|
}
|
|
|
|
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.innerHTML = '<i class="bi bi-' + (paused ? 'play-fill' : '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.id || '';
|
|
}
|
|
pauseForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
|
}
|
|
|
|
var reloadButton = cell.querySelector('button[data-action="reload"]');
|
|
if (reloadButton) {
|
|
reloadButton.innerHTML = '<i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload';
|
|
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.id || '';
|
|
}
|
|
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.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.id || '';
|
|
}
|
|
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');
|
|
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.id || '';
|
|
}
|
|
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');
|
|
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.id || '';
|
|
}
|
|
nextForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
|
}
|
|
}
|
|
|
|
function syncClientActionCell(row, client, hasActionsColumn) {
|
|
if (!row || !row.cells) {
|
|
return;
|
|
}
|
|
|
|
if (!hasActionsColumn) {
|
|
if (row.cells.length > 6) {
|
|
row.deleteCell(row.cells.length - 1);
|
|
}
|
|
return;
|
|
}
|
|
|
|
var actionCell = row.cells.length > 6 ? row.cells[6] : 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 clientIpValue = normalizeDisplayIp(client.clientIp);
|
|
var clientIp = clientIpValue ? escapeHtml(clientIpValue) : '<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.clientId || '') + '" data-client-screen-slug="' + escapeHtml(client.screen_slug || '') + '">',
|
|
'<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="IP">' + clientIp + '</td>',
|
|
'<td data-label="Viewport">' + viewport + '</td>',
|
|
'<td data-label="Connected/Updated">' + connectedAt + '</td>',
|
|
actionCell,
|
|
'</tr>'
|
|
].join('');
|
|
}
|
|
|
|
function renderScreenRow(screen) {
|
|
var clientCount = Number(screen.player_connection_count || 0);
|
|
return [
|
|
'<tr>',
|
|
'<td data-label="Name">' + escapeHtml(screen.name) + '</td>',
|
|
'<td data-label="Player URL"><a href="' + escapeHtml(screen.player_url || '') + '" target="_blank">' + escapeHtml(screen.player_url || '') + '</a></td>',
|
|
'<td data-label="Playlist">' + escapeHtml(screen.playlist_name || '') + '</td>',
|
|
'<td data-label="Connected clients">' + (clientCount ? '<div class="connection-count" data-screen-connection-count="' + escapeHtml(screen.slug) + '">' + clientCount + ' connected</div>' : '<span class="empty">No clients connected.</span>') + '</td>',
|
|
'</tr>'
|
|
].join('');
|
|
}
|
|
|
|
function updateStats(state) {
|
|
var clientCount = document.getElementById('dashboard-client-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 (clientCount) {
|
|
clientCount.textContent = String(Number(state.connectedClientsCount || 0));
|
|
}
|
|
}
|
|
|
|
function updateClientTable(state) {
|
|
var tbody = document.getElementById('dashboard-clients-table-body');
|
|
if (!tbody || !Array.isArray(state.clients)) {
|
|
return;
|
|
}
|
|
var table = document.getElementById('dashboard-clients-table');
|
|
var hasActionsColumn = Boolean(table && String(table.getAttribute('data-has-actions-column') || '').toLowerCase() === 'true');
|
|
if (!state.clients.length) {
|
|
tbody.innerHTML = '<tr><td colspan="' + (hasActionsColumn ? '7' : '6') + '" class="empty">No connected clients yet.</td></tr>';
|
|
return;
|
|
}
|
|
var existingRows = {};
|
|
Array.prototype.slice.call(tbody.querySelectorAll('tr[data-client-key]')).forEach(function (row) {
|
|
existingRows[row.getAttribute('data-client-key')] = row;
|
|
});
|
|
|
|
Array.prototype.slice.call(tbody.querySelectorAll('tr')).forEach(function (row) {
|
|
if (!row.hasAttribute('data-client-key')) {
|
|
row.parentNode.removeChild(row);
|
|
}
|
|
});
|
|
|
|
state.clients.forEach(function (client, index) {
|
|
var rowKey = getClientRowKey(client);
|
|
var row = existingRows[rowKey];
|
|
if (!row) {
|
|
var tempBody = document.createElement('tbody');
|
|
tempBody.innerHTML = renderClientRow(client, hasActionsColumn);
|
|
row = tempBody.firstElementChild;
|
|
}
|
|
|
|
if (!row) {
|
|
return;
|
|
}
|
|
|
|
row.setAttribute('data-client-key', rowKey);
|
|
row.setAttribute('data-client-id', client.clientId || '');
|
|
row.setAttribute('data-client-device-id', client.deviceId || '');
|
|
row.setAttribute('data-client-screen-slug', client.screen_slug || '');
|
|
if (row.cells && row.cells.length >= 6) {
|
|
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 clientIpValue = normalizeDisplayIp(client.clientIp);
|
|
var clientIp = clientIpValue ? escapeHtml(clientIpValue) : '<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.cells[0].innerHTML = '<div>' + clientName + '</div>';
|
|
row.cells[1].innerHTML = '<div>' + screenName + '</div>';
|
|
row.cells[2].innerHTML = currentSlide;
|
|
row.cells[3].innerHTML = clientIp;
|
|
row.cells[4].innerHTML = viewport;
|
|
row.cells[5].innerHTML = connectedAt;
|
|
syncClientActionCell(row, client, hasActionsColumn);
|
|
}
|
|
|
|
var referenceNode = tbody.children[index] || null;
|
|
if (referenceNode !== row) {
|
|
tbody.insertBefore(row, referenceNode);
|
|
}
|
|
});
|
|
|
|
while (tbody.children.length > state.clients.length) {
|
|
tbody.removeChild(tbody.lastElementChild);
|
|
}
|
|
|
|
window.applyTableSort(document.getElementById('dashboard-clients-table'));
|
|
}
|
|
|
|
function updateScreenTable(state) {
|
|
var table = document.getElementById('dashboard-screens-table');
|
|
if (!table || !Array.isArray(state.screens)) {
|
|
return;
|
|
}
|
|
var tbody = table.tBodies && table.tBodies[0] ? table.tBodies[0] : null;
|
|
if (!tbody) {
|
|
return;
|
|
}
|
|
if (!state.screens.length) {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="empty">No screens yet.</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = state.screens.map(renderScreenRow).join('');
|
|
window.applyTableSort(table);
|
|
}
|
|
|
|
function updateDashboardQuickActions(state) {
|
|
var blackoutButton = document.getElementById('dashboard-blackout-all-button');
|
|
if (!blackoutButton || !state || !Array.isArray(state.clients)) {
|
|
return;
|
|
}
|
|
|
|
var hasClients = state.clients.length > 0;
|
|
var allBlackout = hasClients && state.clients.every(function (client) {
|
|
return Boolean(client && client.blackout);
|
|
});
|
|
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'], '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;
|
|
}
|
|
updateStats(state);
|
|
updateScreenTable(state);
|
|
updateClientTable(state);
|
|
updateDashboardQuickActions(state);
|
|
}
|
|
|
|
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-key') || '').trim();
|
|
var clientId = String(row.getAttribute('data-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.');
|
|
});
|
|
});
|
|
}
|
|
|
|
window.webHandleDashboardState = handleDashboardState;
|
|
|
|
initClientRenameHandler();
|
|
}());
|