249 lines
20 KiB
Handlebars
249 lines
20 KiB
Handlebars
<div class="page-header clients-page-header">
|
|
<div>
|
|
<h2>Connected clients</h2>
|
|
<p>Inspect live player connections and send commands to individual clients.</p>
|
|
</div>
|
|
</div>
|
|
|
|
{{#if (anyPermission currentUser 'clients.allow' 'pairing.allow')}}
|
|
<div class="card card-outline card-secondary mb-4 screen-command-card clients-screen-command-card">
|
|
<div class="card-header">
|
|
<div class="dashboard-card-heading">
|
|
<h3 class="card-title">Screen Group controls</h3>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="screen-command-panel{{#if (hasPermission currentUser 'clients.allow')}}{{else}} screen-command-panel-pairing-only{{/if}}">
|
|
{{#if (hasPermission currentUser 'clients.allow')}}
|
|
{{#if screens.length}}
|
|
<div class="screen-command-panel-left">
|
|
<div class="screen-command-selector-field">
|
|
<label class="screen-command-selector-label" for="screen-command-select">Target screen group</label>
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text" aria-hidden="true"><i class="bi bi-broadcast"></i></span>
|
|
<select
|
|
id="screen-command-select"
|
|
class="form-select"
|
|
aria-label="Target screen group"
|
|
data-screen-command-select
|
|
>
|
|
<option value="" selected disabled>Select Screen Group</option>
|
|
{{#each screens}}
|
|
<option
|
|
value="{{slug}}"
|
|
data-screen-name="{{name}}"
|
|
data-player-connection-count="{{player_connection_count}}"
|
|
data-playlist-name="{{playlist_name}}"
|
|
>{{name}}</option>
|
|
{{/each}}
|
|
<option value="__all__" data-screen-name="All screen groups" data-screen-target-all="true">All screen groups</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="screen-command-actions">
|
|
<form method="post" action="#" class="inline-form" data-confirm-message="Reload selected screen?" data-screen-command-form data-screen-command-action="reload" data-async-command>
|
|
<input type="hidden" name="command" value="reload" />
|
|
<button type="submit" class="btn btn-sm btn-danger" disabled><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload screen</button>
|
|
</form>
|
|
<form method="post" action="#" class="inline-form" data-confirm-message="Pause selected screen?" data-screen-command-form data-screen-command-action="pause" data-async-command>
|
|
<input type="hidden" name="command" value="pause" />
|
|
<input type="hidden" name="paused" value="true" />
|
|
<button type="submit" class="btn btn-sm btn-info" disabled><i class="bi bi-pause-fill me-1" aria-hidden="true"></i>Pause screen</button>
|
|
</form>
|
|
<form method="post" action="#" class="inline-form" data-confirm-message="Blackout selected screen?" data-screen-command-form data-screen-command-action="blackout" data-async-command>
|
|
<input type="hidden" name="command" value="blackout" />
|
|
<input type="hidden" name="blackout" value="true" />
|
|
<button type="submit" class="btn btn-sm btn-secondary" disabled><i class="bi bi-eye-slash me-1" aria-hidden="true"></i>Blackout screen</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
<div class="screen-command-panel-right">
|
|
<div class="screen-command-summary">
|
|
<span class="screen-command-pill is-live" data-screen-command-pill>Choose a screen</span>
|
|
<strong data-screen-command-name>Select a target screen group</strong>
|
|
<span class="screen-command-summary-meta" data-screen-command-meta>Commands update automatically for the selected screen group.</span>
|
|
</div>
|
|
</div>
|
|
{{else}}
|
|
<div class="empty">No screen groups are available to target yet.</div>
|
|
{{/if}}
|
|
{{/if}}
|
|
{{#if (hasPermission currentUser 'pairing.allow')}}
|
|
<div class="screen-command-pairing">
|
|
<div>
|
|
<strong>New player?</strong>
|
|
<span class="screen-command-summary-meta">Open the pairing page to connect another display.</span>
|
|
</div>
|
|
<a class="btn btn-primary btn-sm" href="/pairing"><i class="bi bi-qr-code-scan me-1" aria-hidden="true"></i>Pair a player</a>
|
|
</div>
|
|
{{/if}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{{/if}}
|
|
|
|
<div class="card card-outline card-primary clients-client-table-card" data-table-search-container data-table-pagination-card>
|
|
<div class="card-header">
|
|
<h3 class="card-title">Active connections</h3>
|
|
<div class="card-tools d-flex flex-nowrap align-items-center gap-2">
|
|
<div class="input-group input-group-sm flex-shrink-1 table-search-group" style="width: min(14rem, 100%);">
|
|
<span class="input-group-text" role="button" tabindex="0" aria-label="Search" data-table-search-toggle><i class="bi bi-search"></i></span>
|
|
<input type="search" class="form-control" placeholder="Search clients" aria-label="Search clients" data-table-search />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body table-responsive p-0">
|
|
<table id="dashboard-clients-table" class="table table-striped w-100 mb-0" data-has-actions-column="{{#if (hasPermission currentUser 'clients.allow')}}true{{else}}false{{/if}}" data-table-searchable>
|
|
<thead>
|
|
<tr>
|
|
<th data-table-sort-key="client">Client</th>
|
|
<th data-table-sort-key="screen">Current Screen Group</th>
|
|
<th data-table-sort-key="slide">Current Slide</th>
|
|
<th data-table-sort-key="viewport">Viewport</th>
|
|
<th data-table-sort-key="connected">Connected/Updated</th>
|
|
{{#if (hasPermission currentUser 'clients.allow')}}<th>Actions</th>{{/if}}
|
|
</tr>
|
|
</thead>
|
|
<tbody id="dashboard-clients-table-body">
|
|
{{#if clients.length}}
|
|
{{#each clients}}
|
|
<tr data-table-search-row data-client-key="{{id}}" data-client-id="{{id}}" data-client-client-id="{{clientId}}" data-client-device-id="{{deviceId}}" data-client-screen-slug="{{screen_slug}}" data-client-player-base-url="{{player_url}}">
|
|
<td data-label="Client" class="client-rename-cell" title="Double-click to rename">
|
|
<div>{{#if client_name}}{{client_name}}{{else}}Unknown{{/if}}</div>
|
|
</td>
|
|
<td data-label="Current Screen Group">
|
|
<div>{{#if screen_name}}{{screen_name}}{{else}}Unknown{{/if}}</div>
|
|
</td>
|
|
<td data-label="Current Slide">
|
|
{{#if currentSlideTitle}}
|
|
<div>{{currentSlideTitle}}</div>
|
|
{{else}}
|
|
<span class="empty">No slide currently showing</span>
|
|
{{/if}}
|
|
</td>
|
|
<td data-label="Viewport">
|
|
{{#if viewport}}
|
|
{{viewport.width}}x{{viewport.height}}
|
|
{{else}}
|
|
<span class="empty">Unknown</span>
|
|
{{/if}}
|
|
</td>
|
|
<td data-label="Connected/Updated">
|
|
{{#if connectedAt}}
|
|
<div>{{connectedAtLabel}}</div>
|
|
{{#if lastSeenAt}}
|
|
<div class="subtle connected-updated-secondary">{{lastSeenAtLabel}}</div>
|
|
{{/if}}
|
|
{{else}}
|
|
<span class="empty">Unknown</span>
|
|
{{/if}}
|
|
</td>
|
|
{{#if (hasPermission ../currentUser 'clients.allow')}}
|
|
<td data-label="Actions" class="text-end">
|
|
<div class="actions justify-content-end">
|
|
<form method="post" action="/clients/{{screen_slug}}/commands" class="inline-form" data-confirm-message="Reloading will restart the player page. Continue?" data-async-command>
|
|
<input type="hidden" name="command" value="reload" />
|
|
<input type="hidden" name="connectionId" value="{{id}}" />
|
|
<input type="hidden" name="playerBaseUrl" value="{{player_url}}" />
|
|
<button type="submit" class="btn btn-sm btn-danger" 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 group" title="Move client to another screen group"><i class="bi bi-display" aria-hidden="true"></i></button>
|
|
<form method="post" action="/clients/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
|
<input type="hidden" name="command" value="previous" />
|
|
<input type="hidden" name="connectionId" value="{{id}}" />
|
|
<input type="hidden" name="playerBaseUrl" value="{{player_url}}" />
|
|
<button type="submit" class="btn btn-sm btn-warning" 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/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
|
<input type="hidden" name="command" value="next" />
|
|
<input type="hidden" name="connectionId" value="{{id}}" />
|
|
<input type="hidden" name="playerBaseUrl" value="{{player_url}}" />
|
|
<button type="submit" class="btn btn-sm btn-warning" 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/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
|
<input type="hidden" name="command" value="pause" />
|
|
<input type="hidden" name="connectionId" value="{{id}}" />
|
|
<input type="hidden" name="playerBaseUrl" value="{{player_url}}" />
|
|
<button type="submit" class="btn btn-sm btn-info" aria-label="Pause client" title="Pause client"><i class="bi bi-pause-fill me-1" aria-hidden="true"></i>Pause</button>
|
|
</form>
|
|
<form method="post" action="/clients/{{screen_slug}}/commands" class="inline-form" data-async-command>
|
|
<input type="hidden" name="command" value="blackout" />
|
|
<input type="hidden" name="connectionId" value="{{id}}" />
|
|
<input type="hidden" name="playerBaseUrl" value="{{player_url}}" />
|
|
<button type="submit" class="btn btn-sm {{#if blackout}}btn-success{{else}}btn-secondary{{/if}}" aria-label="{{#if blackout}}Restore client{{else}}Blackout client{{/if}}" title="{{#if blackout}}Restore client{{else}}Blackout client{{/if}}"><i class="bi {{#if blackout}}bi-eye{{else}}bi-eye-slash{{/if}} me-1" aria-hidden="true"></i>{{#if blackout}}Restore{{else}}Blackout{{/if}}</button>
|
|
</form>
|
|
</div>
|
|
</td>
|
|
{{/if}}
|
|
</tr>
|
|
{{/each}}
|
|
{{else}}
|
|
<tr data-table-search-empty-default><td colspan="{{#if (hasPermission currentUser 'clients.allow')}}6{{else}}5{{/if}}" class="empty">No connected clients yet.</td></tr>
|
|
{{/if}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{{> table-pagination pagination=pagination basePath="/clients" alwaysShow=true}}
|
|
</div>
|
|
|
|
<section class="clients-mobile-clients" aria-label="Active connections">
|
|
<div class="clients-mobile-client-tools mb-4 mt-0">
|
|
<div><h3>Active connections</h3></div>
|
|
<div class="clients-mobile-client-search input-group input-group-sm"><label for="clients-mobile-search">Search clients</label><span class="input-group-text" role="button" tabindex="0" aria-label="Search clients" data-mobile-client-search-toggle><i class="bi bi-search" aria-hidden="true"></i></span><input id="clients-mobile-search" class="form-control form-control-sm" type="search" placeholder="Search clients" /></div>
|
|
</div>
|
|
<div id="clients-mobile-client-list">
|
|
{{#if clients.length}}
|
|
{{#each clients}}
|
|
<article class="card card-outline {{#if paused}}card-warning{{else}}card-primary{{/if}} clients-mobile-client" data-mobile-client-id="{{id}}" data-mobile-client-client-id="{{clientId}}" data-mobile-client-key="{{id}}" data-client-id="{{id}}" data-client-client-id="{{clientId}}" data-client-screen-slug="{{screen_slug}}" data-client-player-base-url="{{player_url}}" data-mobile-client-search="{{client_name}} {{clientId}} {{screen_name}} {{screen_slug}}">
|
|
<div class="card-body">
|
|
<div class="clients-mobile-client-header"><div><h4 data-mobile-client-name>{{#if client_name}}{{client_name}}{{else}}Unknown{{/if}}</h4></div><span class="badge {{#if blackout}}text-bg-secondary{{else if paused}}text-bg-warning{{else}}text-bg-success{{/if}}">{{#if blackout}}Blackout{{else if paused}}Paused{{else}}Live{{/if}}</span></div>
|
|
<div class="clients-mobile-client-details"><div><small>Screen group</small><strong>{{#if screen_name}}{{screen_name}}{{else}}{{screen_slug}}{{/if}}</strong></div><div><small>Now showing</small><strong>{{#if currentSlideTitle}}{{currentSlideTitle}}{{else}}No slide currently showing{{/if}}</strong></div></div>
|
|
{{#if (hasPermission ../currentUser 'clients.allow')}}
|
|
<div class="clients-mobile-client-actions"><form method="post" action="/clients/{{screen_slug}}/commands" class="inline-form" data-confirm-message="Reloading will restart the player page. Continue?" data-async-command><input type="hidden" name="command" value="reload" /><input type="hidden" name="connectionId" value="{{id}}" /><input type="hidden" name="playerBaseUrl" value="{{player_url}}" /><button type="submit" class="btn btn-sm btn-danger" aria-label="Reload client" title="Reload client"><i class="bi bi-arrow-repeat" aria-hidden="true"></i></button></form><form method="post" action="/clients/{{screen_slug}}/commands" class="inline-form" data-async-command><input type="hidden" name="command" value="previous" /><input type="hidden" name="connectionId" value="{{id}}" /><input type="hidden" name="playerBaseUrl" value="{{player_url}}" /><button type="submit" class="btn btn-sm btn-warning" 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/{{screen_slug}}/commands" class="inline-form" data-async-command><input type="hidden" name="command" value="next" /><input type="hidden" name="connectionId" value="{{id}}" /><input type="hidden" name="playerBaseUrl" value="{{player_url}}" /><button type="submit" class="btn btn-sm btn-warning" 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/{{screen_slug}}/commands" class="inline-form wide" data-async-command><input type="hidden" name="command" value="pause" /><input type="hidden" name="connectionId" value="{{id}}" /><input type="hidden" name="playerBaseUrl" value="{{player_url}}" /><button type="submit" class="btn btn-sm btn-info w-100" aria-label="Pause client" title="Pause client"><i class="bi {{#if paused}}bi-play-fill{{else}}bi-pause-fill{{/if}} me-1" aria-hidden="true"></i>{{#if paused}}Resume{{else}}Pause{{/if}}</button></form><form method="post" action="/clients/{{screen_slug}}/commands" class="inline-form wide" data-async-command><input type="hidden" name="command" value="blackout" /><input type="hidden" name="blackout" value="{{#if blackout}}false{{else}}true{{/if}}" /><input type="hidden" name="connectionId" value="{{id}}" /><input type="hidden" name="playerBaseUrl" value="{{player_url}}" /><button type="submit" class="btn btn-sm {{#if blackout}}btn-success{{else}}btn-secondary{{/if}} w-100" aria-label="{{#if blackout}}Restore client{{else}}Blackout client{{/if}}" title="{{#if blackout}}Restore client{{else}}Blackout client{{/if}}"><i class="bi {{#if blackout}}bi-eye{{else}}bi-eye-slash{{/if}} me-1" aria-hidden="true"></i>{{#if blackout}}Restore{{else}}Blackout{{/if}}</button></form></div>
|
|
{{/if}}
|
|
</div>
|
|
</article>
|
|
{{/each}}
|
|
{{else}}<div class="alert alert-secondary">No connected clients yet.</div>{{/if}}
|
|
</div>
|
|
<div class="alert alert-secondary" id="clients-mobile-empty" hidden>No clients match that search.</div>
|
|
</section>
|
|
<script>
|
|
(function () { var input = document.getElementById('clients-mobile-search'); var toggle = document.querySelector('[data-mobile-client-search-toggle]'); if (input && toggle) { var focusInput = function (event) { if (event && event.preventDefault) { event.preventDefault(); } input.focus(); }; toggle.addEventListener('mousedown', focusInput); toggle.addEventListener('click', focusInput); toggle.addEventListener('keydown', function (event) { if (event.key === 'Enter' || event.key === ' ') { focusInput(event); } }); } }());
|
|
</script>
|
|
<script>
|
|
(function () { var input = document.getElementById('clients-mobile-search'); var empty = document.getElementById('clients-mobile-empty'); if (input) { input.addEventListener('input', function () { var query = input.value.trim().toLowerCase(); var visible = 0; document.querySelectorAll('[data-mobile-client-search]').forEach(function (card) { var matches = !query || card.getAttribute('data-mobile-client-search').toLowerCase().indexOf(query) !== -1; card.hidden = !matches; if (matches) visible += 1; }); if (empty) empty.hidden = visible > 0; }); } }());
|
|
</script>
|
|
{{#> modal-shell modalId="client-move-screen-modal" modalLabelId="client-move-screen-modal-label" modalDialogClass="modal-dialog-centered modal-dialog-scrollable modal-lg" modalBackdropStatic=true modalKeyboardDisabled=true}}
|
|
<form id="client-move-screen-form" method="post" action="#" class="d-flex flex-column">
|
|
<div class="modal-header">
|
|
<div>
|
|
<h2 class="modal-title fs-5" id="client-move-screen-modal-label">Move client to another screen group</h2>
|
|
</div>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<input type="hidden" name="command" value="moveclient" />
|
|
<input type="hidden" name="connectionId" value="" data-client-move-connection-id />
|
|
<input type="hidden" name="clientId" value="" data-client-move-client-id />
|
|
<input type="hidden" name="clientName" value="" data-client-move-client-name />
|
|
<input type="hidden" name="playerBaseUrl" value="" data-client-move-player-base-url />
|
|
<label class="form-label" for="client-move-screen-target">Target screen group</label>
|
|
<select class="form-select" id="client-move-screen-target" name="targetScreenSlug" data-client-move-target required>
|
|
{{#if screens.length}}
|
|
<option value="">Select a screen group</option>
|
|
{{#each screens}}
|
|
<option value="{{slug}}">{{name}}</option>
|
|
{{/each}}
|
|
{{else}}
|
|
<option value="">No screen groups available</option>
|
|
{{/if}}
|
|
</select>
|
|
</div>
|
|
<div class="modal-footer justify-content-end gap-2">
|
|
<button type="submit" class="btn btn-primary">Move client</button>
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</form>
|
|
{{/modal-shell}}
|