Add player control-plane and dashboard updates
This commit is contained in:
@@ -6,7 +6,169 @@
|
||||
var getClientDisplayName = webUiHelpers.getClientDisplayName;
|
||||
var setButtonVariant = webUiHelpers.setButtonVariant;
|
||||
var normalizeDisplayIp = webUiHelpers.normalizeDisplayIp;
|
||||
var LIST_PAGE_SIZE = 25;
|
||||
var latestDashboardState = null;
|
||||
var ALL_SCREENS_SLUG = '__all__';
|
||||
var ALL_SCREENS_LABEL = 'All screens';
|
||||
|
||||
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 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.ipAddress,
|
||||
client && client.clientIp,
|
||||
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(); },
|
||||
ip: function (client) { return String(client && client.clientIp || '').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);
|
||||
}
|
||||
|
||||
var sortKeys = accessors[normalizedSortKey]
|
||||
? [normalizedSortKey]
|
||||
: ['client'];
|
||||
|
||||
if (sortKeys[0] === 'client') {
|
||||
sortKeys.push('ip');
|
||||
} else if (sortKeys[0] === 'ip') {
|
||||
sortKeys.push('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 = 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 getClientMoveModalElements() {
|
||||
return {
|
||||
@@ -15,7 +177,8 @@
|
||||
targetSelect: document.getElementById('client-move-screen-target'),
|
||||
connectionInput: document.querySelector('[data-client-move-connection-id]'),
|
||||
deviceInput: document.querySelector('[data-client-move-device-id]'),
|
||||
clientNameInput: document.querySelector('[data-client-move-client-name]')
|
||||
clientNameInput: document.querySelector('[data-client-move-client-name]'),
|
||||
playerBaseUrlInput: document.querySelector('[data-client-move-player-base-url]')
|
||||
};
|
||||
}
|
||||
|
||||
@@ -48,6 +211,7 @@
|
||||
var currentScreenSlug = String(row.getAttribute('data-client-screen-slug') || '').trim();
|
||||
var connectionId = String(row.getAttribute('data-client-key') || '').trim();
|
||||
var deviceId = String(row.getAttribute('data-client-device-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 || []);
|
||||
@@ -69,6 +233,9 @@
|
||||
if (elements.clientNameInput) {
|
||||
elements.clientNameInput.value = clientName;
|
||||
}
|
||||
if (elements.playerBaseUrlInput) {
|
||||
elements.playerBaseUrlInput.value = playerBaseUrl;
|
||||
}
|
||||
elements.targetSelect.value = '';
|
||||
if (elements.form.querySelector('button[type="submit"]')) {
|
||||
elements.form.querySelector('button[type="submit"]').disabled = false;
|
||||
@@ -89,12 +256,12 @@
|
||||
|
||||
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" aria-label="Reload client" title="Reload client"><i class="bi bi-arrow-repeat" aria-hidden="true"></i></button></form>',
|
||||
'<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) + '" /><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" data-bs-toggle="modal" data-bs-target="#client-move-screen-modal" 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(client.id) + '" /><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(client.id) + '" /><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(client.id) + '" /><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(client.id) + '" /><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>',
|
||||
'<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) + '" /><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(client.id) + '" /><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(client.id) + '" /><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(client.id) + '" /><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('');
|
||||
}
|
||||
@@ -126,6 +293,10 @@
|
||||
if (connectionInput) {
|
||||
connectionInput.value = client.id || '';
|
||||
}
|
||||
var playerBaseUrlInput = pauseForm.querySelector('input[name="playerBaseUrl"]');
|
||||
if (playerBaseUrlInput) {
|
||||
playerBaseUrlInput.value = client.player_url || '';
|
||||
}
|
||||
pauseForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
}
|
||||
|
||||
@@ -141,6 +312,10 @@
|
||||
if (reloadInput) {
|
||||
reloadInput.value = 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?');
|
||||
}
|
||||
@@ -173,6 +348,10 @@
|
||||
if (blackoutConnectionInput) {
|
||||
blackoutConnectionInput.value = client.id || '';
|
||||
}
|
||||
var blackoutPlayerBaseUrlInput = blackoutForm.querySelector('input[name="playerBaseUrl"]');
|
||||
if (blackoutPlayerBaseUrlInput) {
|
||||
blackoutPlayerBaseUrlInput.value = client.player_url || '';
|
||||
}
|
||||
blackoutForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
}
|
||||
|
||||
@@ -194,6 +373,10 @@
|
||||
if (previousConnectionInput) {
|
||||
previousConnectionInput.value = client.id || '';
|
||||
}
|
||||
var previousPlayerBaseUrlInput = previousForm.querySelector('input[name="playerBaseUrl"]');
|
||||
if (previousPlayerBaseUrlInput) {
|
||||
previousPlayerBaseUrlInput.value = client.player_url || '';
|
||||
}
|
||||
previousForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
}
|
||||
|
||||
@@ -215,6 +398,10 @@
|
||||
if (nextConnectionInput) {
|
||||
nextConnectionInput.value = client.id || '';
|
||||
}
|
||||
var nextPlayerBaseUrlInput = nextForm.querySelector('input[name="playerBaseUrl"]');
|
||||
if (nextPlayerBaseUrlInput) {
|
||||
nextPlayerBaseUrlInput.value = client.player_url || '';
|
||||
}
|
||||
nextForm.action = '/clients/' + encodeURIComponent(client.screen_slug) + '/commands';
|
||||
}
|
||||
}
|
||||
@@ -253,7 +440,7 @@
|
||||
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-device-id="' + escapeHtml(client.deviceId || '') + '" data-client-screen-slug="' + escapeHtml(client.screen_slug || '') + '">',
|
||||
'<tr data-client-key="' + escapeHtml(getClientRowKey(client)) + '" data-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>',
|
||||
@@ -265,26 +452,67 @@
|
||||
].join('');
|
||||
}
|
||||
|
||||
function updateClientRowCells(row, client, hasActionsColumn) {
|
||||
if (!row || !row.cells || row.cells.length < 6) {
|
||||
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 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.setAttribute('data-client-key', escapeHtml(getClientRowKey(client)));
|
||||
row.setAttribute('data-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], clientIp);
|
||||
setCellHtml(row.cells[4], viewport);
|
||||
setCellHtml(row.cells[5], 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 playerUrl = String(screen.player_url || '').trim();
|
||||
var connectionLabel = clientCount ? clientCount + ' live' : 'No clients';
|
||||
var connectionStateClass = clientCount ? 'is-live' : 'is-idle';
|
||||
var playlistLabel = screen.playlist_name ? escapeHtml(screen.playlist_name) : 'Unassigned';
|
||||
var connectionsLabel = clientCount ? clientCount + ' connected' : 'No clients connected';
|
||||
|
||||
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>',
|
||||
'<a class="dashboard-screen-link" href="' + escapeHtml(playerUrl) + '" target="_blank">' + escapeHtml(playerUrl) + '</a>',
|
||||
'</div>',
|
||||
'<span class="dashboard-screen-pill ' + connectionStateClass + '">' + escapeHtml(connectionLabel) + '</span>',
|
||||
'</div>',
|
||||
'<dl class="dashboard-screen-meta">',
|
||||
'<div><dt>Playlist</dt><dd>' + playlistLabel + '</dd></div>',
|
||||
'<div><dt>Connections</dt><dd>' + escapeHtml(connectionsLabel) + '</dd></div>',
|
||||
'</dl>',
|
||||
'</article>'
|
||||
].join('');
|
||||
@@ -310,7 +538,7 @@
|
||||
}
|
||||
|
||||
function updateStats(state) {
|
||||
var clientCount = document.getElementById('dashboard-client-count');
|
||||
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');
|
||||
@@ -324,81 +552,137 @@
|
||||
if (screenCount && Array.isArray(state.screens)) {
|
||||
screenCount.textContent = String(state.screens.length);
|
||||
}
|
||||
if (clientCount) {
|
||||
clientCount.textContent = String(Number(state.connectedClientsCount || 0));
|
||||
if (playerCount) {
|
||||
playerCount.textContent = String(Number(state.connectedPlayersCount || 0));
|
||||
}
|
||||
}
|
||||
|
||||
function updateClientTable(state) {
|
||||
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');
|
||||
if (!state.clients.length) {
|
||||
var visibleClients = getVisibleClients(state);
|
||||
|
||||
var canPatchRows = typeof tbody.querySelectorAll === 'function'
|
||||
&& typeof tbody.insertBefore === 'function'
|
||||
&& typeof tbody.removeChild === 'function'
|
||||
&& typeof document.createElement === 'function';
|
||||
|
||||
if (!visibleClients.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="' + (hasActionsColumn ? '7' : '6') + '" class="empty">No connected clients yet.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
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[row.getAttribute('data-client-key')] = row;
|
||||
existingRows[String(row.getAttribute('data-client-key') || '').trim()] = row;
|
||||
});
|
||||
|
||||
Array.prototype.slice.call(tbody.querySelectorAll('tr')).forEach(function (row) {
|
||||
if (!row.hasAttribute('data-client-key')) {
|
||||
row.parentNode.removeChild(row);
|
||||
var nextRows = visibleClients.map(function (client) {
|
||||
var rowKey = String(getClientRowKey(client) || '').trim();
|
||||
var row = existingRows[rowKey] || null;
|
||||
|
||||
if (!row) {
|
||||
row = createClientRowFromTemplate(client, hasActionsColumn);
|
||||
} else {
|
||||
updateClientRowCells(row, client, hasActionsColumn);
|
||||
}
|
||||
|
||||
return row;
|
||||
}).filter(function (row) {
|
||||
return Boolean(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);
|
||||
}
|
||||
|
||||
nextRows.forEach(function (row, index) {
|
||||
var referenceNode = tbody.children[index] || null;
|
||||
if (referenceNode !== row) {
|
||||
tbody.insertBefore(row, referenceNode);
|
||||
}
|
||||
});
|
||||
|
||||
while (tbody.children.length > state.clients.length) {
|
||||
while (tbody.children.length > nextRows.length) {
|
||||
tbody.removeChild(tbody.lastElementChild);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function refreshClientTableFromLatestState() {
|
||||
if (!latestDashboardState) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateClientTable(latestDashboardState, true);
|
||||
}
|
||||
|
||||
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)) {
|
||||
@@ -455,13 +739,22 @@
|
||||
}
|
||||
|
||||
select.disabled = false;
|
||||
if (!select.value || !screenBySlug[select.value]) {
|
||||
select.value = screens[0].slug || '';
|
||||
}
|
||||
|
||||
var selectedScreen = screenBySlug[select.value] || screens[0];
|
||||
var selectedSlug = String(selectedScreen && selectedScreen.slug || '').trim();
|
||||
var selectedOption = select.options && select.selectedIndex >= 0 ? select.options[select.selectedIndex] : null;
|
||||
var isAllSelected = Boolean(selectedOption && String(selectedOption.getAttribute('data-screen-target-all') || '').toLowerCase() === 'true') || select.value === ALL_SCREENS_SLUG;
|
||||
var selectedScreen = isAllSelected
|
||||
? {
|
||||
slug: ALL_SCREENS_SLUG,
|
||||
name: String(selectedOption && selectedOption.textContent || ALL_SCREENS_LABEL).trim() || ALL_SCREENS_LABEL
|
||||
}
|
||||
: screenBySlug[select.value] || null;
|
||||
var selectedSlug = isAllSelected
|
||||
? ALL_SCREENS_SLUG
|
||||
: String(selectedScreen && selectedScreen.slug || '').trim();
|
||||
var selectedClients = Array.isArray(state && state.clients) ? state.clients.filter(function (client) {
|
||||
if (isAllSelected) {
|
||||
return true;
|
||||
}
|
||||
return String(client && client.screen_slug || '').trim() === selectedSlug;
|
||||
}) : [];
|
||||
var connectionCount = selectedClients.length;
|
||||
@@ -480,12 +773,20 @@
|
||||
pill.textContent = connectionLabel;
|
||||
}
|
||||
if (nameNode) {
|
||||
nameNode.textContent = String(selectedScreen && selectedScreen.name || 'Selected screen');
|
||||
nameNode.textContent = selectedScreen
|
||||
? String(selectedScreen.name || 'Selected screen')
|
||||
: 'Select a target screen group';
|
||||
}
|
||||
if (metaNode) {
|
||||
metaNode.textContent = 'Commands sent here target every client currently using this screen.';
|
||||
metaNode.textContent = !selectedSlug
|
||||
? 'Choose a screen group before sending commands.'
|
||||
: isAllSelected
|
||||
? 'Commands sent here target every client across every screen group.'
|
||||
: 'Commands sent here target every client currently using this screen.';
|
||||
}
|
||||
|
||||
var commandTargetSlug = selectedSlug || '';
|
||||
|
||||
forms.forEach(function (form) {
|
||||
var command = String(form.getAttribute('data-screen-command-action') || '').trim().toLowerCase();
|
||||
var commandInput = form.querySelector('input[name="command"]');
|
||||
@@ -498,10 +799,12 @@
|
||||
pauseStateInput.value = allPaused ? 'false' : 'true';
|
||||
}
|
||||
if (button) {
|
||||
button.innerHTML = '<i class="bi ' + (allPaused ? 'bi-play-fill' : 'bi-pause-fill') + ' me-1" aria-hidden="true"></i>' + (allPaused ? 'Resume screen' : 'Pause screen');
|
||||
button.innerHTML = '<i class="bi ' + (allPaused ? 'bi-play-fill' : 'bi-pause-fill') + ' me-1" aria-hidden="true"></i>' + (allPaused ? (isAllSelected ? 'Resume all screens' : 'Resume screen') : (isAllSelected ? 'Pause all screens' : 'Pause screen'));
|
||||
}
|
||||
setButtonVariant(button, ['btn-success', 'btn-info', 'btn-secondary', 'btn-outline-secondary', 'btn-outline-dark'], allPaused ? 'btn-success' : 'btn-info');
|
||||
form.setAttribute('data-confirm-message', allPaused ? 'Resume all connected clients on this screen?' : 'Pause all connected clients on this screen?');
|
||||
form.setAttribute('data-confirm-message', allPaused
|
||||
? (isAllSelected ? 'Resume all connected clients on all screens?' : 'Resume all connected clients on this screen?')
|
||||
: (isAllSelected ? 'Pause all connected clients on all screens?' : 'Pause all connected clients on this screen?'));
|
||||
} else if (command === 'blackout') {
|
||||
commandInput.value = 'blackout';
|
||||
var blackoutStateInput = form.querySelector('input[name="blackout"]');
|
||||
@@ -509,23 +812,25 @@
|
||||
blackoutStateInput.value = allBlackout ? 'false' : 'true';
|
||||
}
|
||||
if (button) {
|
||||
button.innerHTML = '<i class="bi ' + (allBlackout ? 'bi-eye' : 'bi-eye-slash') + ' me-1" aria-hidden="true"></i>' + (allBlackout ? 'Restore screen' : 'Blackout screen');
|
||||
button.innerHTML = '<i class="bi ' + (allBlackout ? 'bi-eye' : 'bi-eye-slash') + ' me-1" aria-hidden="true"></i>' + (allBlackout ? (isAllSelected ? 'Restore all screens' : 'Restore screen') : (isAllSelected ? 'Blackout all screens' : 'Blackout screen'));
|
||||
}
|
||||
setButtonVariant(button, ['btn-success', 'btn-secondary', 'btn-danger', 'btn-outline-secondary', 'btn-outline-dark'], allBlackout ? 'btn-success' : 'btn-secondary');
|
||||
form.setAttribute('data-confirm-message', allBlackout ? 'Restore all connected clients on this screen?' : 'Blackout all connected clients on this screen?');
|
||||
form.setAttribute('data-confirm-message', allBlackout
|
||||
? (isAllSelected ? 'Restore all connected clients on all screens?' : 'Restore all connected clients on this screen?')
|
||||
: (isAllSelected ? 'Blackout all connected clients on all screens?' : 'Blackout all connected clients on this screen?'));
|
||||
} else {
|
||||
commandInput.value = command || commandInput.value || '';
|
||||
}
|
||||
}
|
||||
form.action = selectedSlug ? '/clients/' + encodeURIComponent(selectedSlug) + '/commands' : '#';
|
||||
form.action = commandTargetSlug ? '/clients/' + encodeURIComponent(commandTargetSlug) + '/commands' : '#';
|
||||
if (command === 'reload') {
|
||||
form.setAttribute('data-confirm-message', 'Reload selected screen?');
|
||||
form.setAttribute('data-confirm-message', isAllSelected ? 'Reload all screens?' : 'Reload selected screen?');
|
||||
if (button) {
|
||||
button.innerHTML = '<i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload screen';
|
||||
button.innerHTML = '<i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>' + (isAllSelected ? 'Reload all screens' : 'Reload screen');
|
||||
}
|
||||
}
|
||||
Array.prototype.slice.call(form.querySelectorAll('button, input')).forEach(function (control) {
|
||||
control.disabled = !selectedSlug;
|
||||
control.disabled = !commandTargetSlug;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -599,10 +904,12 @@
|
||||
return;
|
||||
}
|
||||
latestDashboardState = state;
|
||||
window.webLatestDashboardState = latestDashboardState;
|
||||
updateStats(state);
|
||||
updateScreenGrid(state);
|
||||
updateScreenCommandControls(state);
|
||||
updateClientTable(state);
|
||||
updateKioskLauncherModal(state);
|
||||
updateDashboardQuickActions(state);
|
||||
}
|
||||
|
||||
@@ -780,34 +1087,28 @@
|
||||
}
|
||||
|
||||
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 setDownloadsEnabled(enabled) {
|
||||
downloadLinks.forEach(function (link) {
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
|
||||
link.classList.toggle('disabled', !enabled);
|
||||
link.setAttribute('aria-disabled', enabled ? 'false' : 'true');
|
||||
if (enabled) {
|
||||
link.removeAttribute('tabindex');
|
||||
} else {
|
||||
link.setAttribute('tabindex', '-1');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function resetModalState() {
|
||||
if (checkbox) {
|
||||
checkbox.checked = false;
|
||||
}
|
||||
setDownloadsEnabled(false);
|
||||
if (select) {
|
||||
select.value = '';
|
||||
}
|
||||
updateKioskLauncherModal(latestDashboardState);
|
||||
}
|
||||
|
||||
if (checkbox) {
|
||||
checkbox.addEventListener('change', function () {
|
||||
setDownloadsEnabled(Boolean(checkbox.checked));
|
||||
updateKioskLauncherModal(latestDashboardState || readScreenCommandStateFromDom());
|
||||
});
|
||||
}
|
||||
|
||||
if (select) {
|
||||
select.addEventListener('change', function () {
|
||||
updateKioskLauncherModal(latestDashboardState || readScreenCommandStateFromDom());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -825,6 +1126,7 @@
|
||||
}
|
||||
|
||||
window.webHandleDashboardState = handleDashboardState;
|
||||
window.webRefreshClientTableFromLatestState = refreshClientTableFromLatestState;
|
||||
|
||||
initClientRenameHandler();
|
||||
initClientMoveHandler();
|
||||
|
||||
Reference in New Issue
Block a user