Release 1.5.3

This commit is contained in:
2026-07-25 15:11:41 +01:00
parent 9ee938fd2f
commit 0e89892c94
89 changed files with 7660 additions and 542 deletions
@@ -5,17 +5,21 @@
</div>
</div>
<div class="card card-outline card-primary">
<div class="card card-outline card-primary" data-table-search-container>
<div class="card-header">
<h3 class="card-title">Saved sources</h3>
{{#if (hasPermission currentUser 'api-sources.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/data-sources/api-sources/new">Add API source</a>
<div class="card-tools d-flex flex-nowrap align-items-center gap-2">
<div class="input-group input-group-sm flex-shrink-1" style="width: min(14rem, 100%);">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" placeholder="Search API sources" aria-label="Search API sources" data-table-search />
</div>
{{/if}}
{{#if (hasPermission currentUser 'api-sources.create')}}
<a class="btn btn-primary btn-sm" href="/data-sources/api-sources/new">Add API source</a>
{{/if}}
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<table class="table table-striped w-100 mb-0" data-table-searchable>
<thead>
<tr>
<th>Name</th>
@@ -29,7 +33,7 @@
<tbody>
{{#if apiSources.length}}
{{#each apiSources}}
<tr>
<tr data-table-search-row>
<td data-label="Name">{{name}}</td>
<td data-label="API URL" class="text-break">{{api_url}}</td>
<td data-label="Refresh interval">{{intervalLabel}}</td>
@@ -69,9 +73,10 @@
</tr>
{{/each}}
{{else}}
<tr><td colspan="6" class="empty">No API sources yet.</td></tr>
<tr data-table-search-empty-default><td colspan="6" class="empty">No API sources yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
{{> table-pagination pagination=pagination basePath="/data-sources/api-sources"}}
</div>
+13 -8
View File
@@ -5,17 +5,21 @@
</div>
</div>
<div class="card card-outline card-primary">
<div class="card card-outline card-primary" data-table-search-container>
<div class="card-header">
<h3 class="card-title">Saved feeds</h3>
{{#if (hasPermission currentUser 'rss-feeds.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/data-sources/rss-feeds/new">Add RSS feed</a>
<div class="card-tools d-flex flex-nowrap align-items-center gap-2">
<div class="input-group input-group-sm flex-shrink-1" style="width: min(14rem, 100%);">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" placeholder="Search RSS feeds" aria-label="Search RSS feeds" data-table-search />
</div>
{{/if}}
{{#if (hasPermission currentUser 'rss-feeds.create')}}
<a class="btn btn-primary btn-sm" href="/data-sources/rss-feeds/new">Add RSS feed</a>
{{/if}}
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<table class="table table-striped w-100 mb-0" data-table-searchable>
<thead>
<tr>
<th>Name</th>
@@ -28,7 +32,7 @@
<tbody>
{{#if rssFeeds.length}}
{{#each rssFeeds}}
<tr>
<tr data-table-search-row>
<td data-label="Name">{{name}}</td>
<td data-label="Feed URL" class="text-break">{{feed_url}}</td>
<td data-label="Refresh interval">{{intervalLabel}}</td>
@@ -52,9 +56,10 @@
</tr>
{{/each}}
{{else}}
<tr><td colspan="5" class="empty">No RSS feeds yet.</td></tr>
<tr data-table-search-empty-default><td colspan="5" class="empty">No RSS feeds yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
{{> table-pagination pagination=pagination basePath="/data-sources/rss-feeds"}}
</div>
@@ -3,49 +3,44 @@
<div class="page-header">
<div>
<h2>Background tasks</h2>
<p>Queued data refreshes run in the web process. Finished items stay here until you clear them.</p>
<p>Queued data refreshes run in the web process. Finished items stay here until you clear them. Click a status box or source name to filter the results.</p>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-6 col-xl-3">
<div class="card card-outline card-primary h-100">
<div class="card-body">
<div class="text-muted small text-uppercase">Queued</div>
<div class="fs-3 fw-semibold">{{summary.counts.queued}}</div>
</div>
</div>
</div>
<div class="col-6 col-xl-3">
<div class="card card-outline card-info h-100">
<div class="card-body">
<div class="text-muted small text-uppercase">Running</div>
<div class="fs-3 fw-semibold">{{summary.counts.running}}</div>
</div>
</div>
</div>
<div class="col-6 col-xl-3">
<div class="card card-outline card-danger h-100">
<div class="card-body">
<div class="text-muted small text-uppercase">Failed</div>
<div class="fs-3 fw-semibold">{{summary.counts.failed}}</div>
</div>
</div>
</div>
<div class="col-6 col-xl-3">
<div class="card card-outline card-success h-100">
<div class="card-body">
<div class="text-muted small text-uppercase">Finished</div>
<div class="fs-3 fw-semibold">{{summary.counts.completed}}</div>
</div>
<div id="background-tasks-filters">
{{#if hasActiveFilters}}
<div class="alert alert-light border d-flex flex-wrap align-items-center justify-content-between gap-2 py-2 mb-3">
<div class="small text-muted">
Showing results for {{#if activeStatusLabel}}{{activeStatusLabel}}{{else}}all tasks{{/if}}{{#if activeSourceLabel}} from {{activeSourceLabel}}{{/if}}.
</div>
<a class="btn btn-outline-secondary btn-sm" href="{{clearFiltersUrl}}" data-background-tasks-nav="clear">Clear filters</a>
</div>
{{/if}}
</div>
<div class="card card-outline card-primary mb-4">
<div class="card-header d-flex align-items-center gap-2">
<div id="background-tasks-summary" class="row g-3 mb-4">
{{#each statusCards}}
<div class="col-6 col-xl-3">
<a href="{{href}}" class="d-block h-100 text-decoration-none text-body" data-background-tasks-nav="status" aria-label="{{ariaLabel}}"{{#if active}} aria-current="page"{{/if}}>
<div class="card card-outline card-{{variant}} h-100{{#if active}} border-2 border-{{variant}}{{/if}}">
<div class="card-body">
<div class="text-muted small text-uppercase">{{label}}</div>
<div class="fs-3 fw-semibold">{{count}}</div>
</div>
</div>
</a>
</div>
{{/each}}
</div>
<div id="background-tasks-task-card" class="card card-outline card-primary mb-4" data-pagination-card="tasks" data-table-search-container>
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0">Task queue</h3>
<div class="ms-auto d-flex align-items-center gap-2">
<div class="ms-auto d-flex flex-nowrap align-items-center gap-2 background-tasks-task-tools">
<div class="input-group input-group-sm background-tasks-task-search" style="width: min(12rem, 100%);">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" placeholder="Search tasks" aria-label="Search tasks" data-table-search />
</div>
{{#if canManage}}
{{#if canClearFinished}}
<form method="post" action="/settings/background-tasks/clear-finished">
@@ -53,11 +48,10 @@
</form>
{{/if}}
{{/if}}
<span class="text-muted small">Active workers: {{summary.activeCount}}</span>
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0 align-middle">
<table class="table table-striped w-100 mb-0 align-middle" data-table-searchable>
<thead>
<tr>
<th>Task</th>
@@ -73,7 +67,7 @@
<tbody>
{{#if tasks.length}}
{{#each tasks}}
<tr>
<tr data-table-search-row>
<td data-label="Task">
<div class="fw-semibold">{{title}}</div>
{{#if key}}
@@ -147,7 +141,7 @@
</tr>
{{/each}}
{{else}}
<tr>
<tr data-table-search-empty-default>
<td colspan="8" class="empty">No background tasks are queued right now.</td>
</tr>
{{/if}}
@@ -178,12 +172,18 @@
{{/if}}
</div>
<div class="card card-outline card-secondary">
<div class="card-header">
<div id="background-tasks-recurring-card" class="card card-outline card-secondary" data-pagination-card="recurring" data-table-search-container>
<div class="card-header d-flex align-items-center">
<h3 class="card-title mb-0">Scheduled refreshes</h3>
<div class="ms-auto d-flex flex-nowrap align-items-center gap-2 background-tasks-recurring-tools">
<div class="input-group input-group-sm background-tasks-recurring-search" style="width: min(12rem, 100%);">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" placeholder="Search scheduled" aria-label="Search scheduled refreshes" data-table-search />
</div>
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0 align-middle">
<table class="table table-striped w-100 mb-0 align-middle" data-table-searchable>
<thead>
<tr>
<th>Task</th>
@@ -197,7 +197,7 @@
<tbody>
{{#if recurringTasks.length}}
{{#each recurringTasks}}
<tr>
<tr data-table-search-row>
<td data-label="Task">
<div class="fw-semibold">{{title}}</div>
<div class="text-muted small text-break">{{key}}</div>
@@ -232,7 +232,7 @@
</tr>
{{/each}}
{{else}}
<tr>
<tr data-table-search-empty-default>
<td colspan="6" class="empty">No scheduled refreshes are registered yet.</td>
</tr>
{{/if}}
+13 -8
View File
@@ -7,17 +7,21 @@
<div class="row g-4">
<div class="col-12">
<div class="card card-outline card-primary admin-form-card">
<div class="card card-outline card-primary admin-form-card" data-table-search-container>
<div class="card-header">
<h3 class="card-title">Existing roles</h3>
{{#if (hasPermission currentUser 'rbac.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/rbac/new">Add role</a>
<div class="card-tools d-flex flex-nowrap align-items-center gap-2">
<div class="input-group input-group-sm flex-shrink-1" style="width: min(14rem, 100%);">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" placeholder="Search roles" aria-label="Search roles" data-table-search />
</div>
{{/if}}
{{#if (hasPermission currentUser 'rbac.create')}}
<a class="btn btn-primary btn-sm" href="/rbac/new">Add role</a>
{{/if}}
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0 users-table">
<table class="table table-striped w-100 mb-0 users-table" data-table-searchable>
<thead>
<tr>
<th>Role</th>
@@ -30,7 +34,7 @@
<tbody>
{{#if roles.length}}
{{#each roles}}
<tr>
<tr data-table-search-row>
<td data-label="Role">
<div class="user-cell">
<div>
@@ -68,11 +72,12 @@
</tr>
{{/each}}
{{else}}
<tr><td colspan="5" class="empty">No roles found.</td></tr>
<tr data-table-search-empty-default><td colspan="5" class="empty">No roles found.</td></tr>
{{/if}}
</tbody>
</table>
</div>
{{> table-pagination pagination=pagination basePath="/rbac"}}
</div>
</div>
</div>
+13 -8
View File
@@ -5,17 +5,21 @@
</div>
</div>
<div class="card card-outline card-primary">
<div class="card card-outline card-primary" data-table-search-container>
<div class="card-header">
<h3 class="card-title">Existing users</h3>
{{#if (hasPermission currentUser 'users.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/users/new">Add user</a>
<div class="card-tools d-flex flex-nowrap align-items-center gap-2">
<div class="input-group input-group-sm flex-shrink-1" style="width: min(14rem, 100%);">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" placeholder="Search users" aria-label="Search users" data-table-search />
</div>
{{/if}}
{{#if (hasPermission currentUser 'users.create')}}
<a class="btn btn-primary btn-sm" href="/users/new">Add user</a>
{{/if}}
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0 users-table">
<table class="table table-striped w-100 mb-0 users-table" data-table-searchable>
<thead>
<tr>
<th>User</th>
@@ -27,7 +31,7 @@
<tbody>
{{#if users.length}}
{{#each users}}
<tr>
<tr data-table-search-row>
<td data-label="User">
<div class="user-cell">
<div>
@@ -85,9 +89,10 @@
</tr>
{{/each}}
{{else}}
<tr><td colspan="4" class="empty">No users found.</td></tr>
<tr data-table-search-empty-default><td colspan="4" class="empty">No users found.</td></tr>
{{/if}}
</tbody>
</table>
</div>
{{> table-pagination pagination=pagination basePath="/users"}}
</div>
+7
View File
@@ -0,0 +1,7 @@
<div class="modal fade" id="{{modalId}}" tabindex="-1" aria-labelledby="{{modalLabelId}}" aria-hidden="true"{{#if modalBackdropStatic}} data-bs-backdrop="static"{{/if}}{{#if modalKeyboardDisabled}} data-bs-keyboard="false"{{/if}}>
<div class="modal-dialog{{#if modalDialogClass}} {{modalDialogClass}}{{/if}}">
<div class="modal-content">
{{> @partial-block }}
</div>
</div>
</div>
+22
View File
@@ -0,0 +1,22 @@
{{#if pagination.hasMultiplePages}}
<div class="card-footer d-flex flex-wrap align-items-center justify-content-between gap-3">
<div class="text-muted small">
Showing {{pagination.startItem}}-{{pagination.endItem}} of {{pagination.totalItems}} {{pagination.itemLabel}}
</div>
<nav aria-label="{{pagination.ariaLabel}}">
<ul class="pagination pagination-sm mb-0">
<li class="page-item {{#unless pagination.hasPrevious}}disabled{{/unless}}">
<a class="page-link" href="{{#if pagination.hasPrevious}}{{basePath}}{{pagination.previousUrl}}{{else}}#{{/if}}" aria-label="Previous page">Previous</a>
</li>
{{#each pagination.pages}}
<li class="page-item {{#if active}}active{{/if}}">
<a class="page-link" href="{{../basePath}}{{url}}">{{number}}</a>
</li>
{{/each}}
<li class="page-item {{#unless pagination.hasNext}}disabled{{/unless}}">
<a class="page-link" href="{{#if pagination.hasNext}}{{basePath}}{{pagination.nextUrl}}{{else}}#{{/if}}" aria-label="Next page">Next</a>
</li>
</ul>
</nav>
</div>
{{/if}}
+13 -8
View File
@@ -5,17 +5,21 @@
</div>
</div>
<div class="card card-outline card-primary">
<div class="card card-outline card-primary" data-table-search-container>
<div class="card-header">
<h3 class="card-title">Saved sizes</h3>
{{#if (hasPermission currentUser 'canvas-sizes.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/canvas-sizes/new">Add canvas size</a>
<div class="card-tools d-flex flex-nowrap align-items-center gap-2">
<div class="input-group input-group-sm flex-shrink-1" style="width: min(14rem, 100%);">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" placeholder="Search canvas sizes" aria-label="Search canvas sizes" data-table-search />
</div>
{{/if}}
{{#if (hasPermission currentUser 'canvas-sizes.create')}}
<a class="btn btn-primary btn-sm" href="/canvas-sizes/new">Add canvas size</a>
{{/if}}
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<table class="table table-striped w-100 mb-0" data-table-searchable>
<thead>
<tr>
<th>Name</th>
@@ -27,7 +31,7 @@
<tbody>
{{#if canvasSizes.length}}
{{#each canvasSizes}}
<tr>
<tr data-table-search-row>
<td data-label="Name">{{name}}</td>
<td data-label="Dimensions">{{width}}x{{height}}</td>
<td data-label="Templates">{{templateCount}}</td>
@@ -50,9 +54,10 @@
</tr>
{{/each}}
{{else}}
<tr><td colspan="4" class="empty">No canvas sizes yet.</td></tr>
<tr data-table-search-empty-default><td colspan="4" class="empty">No canvas sizes yet.</td></tr>
{{/if}}
</tbody>
</table>
{{> table-pagination pagination=pagination basePath="/canvas-sizes"}}
</div>
+11 -4
View File
@@ -5,12 +5,18 @@
</div>
</div>
<div class="card card-outline card-primary">
<div class="card card-outline card-primary" data-table-search-container>
<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" style="width: min(14rem, 100%);">
<span class="input-group-text" aria-hidden="true"><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}}">
<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>Client</th>
@@ -25,7 +31,7 @@
<tbody id="dashboard-clients-table-body">
{{#if clients.length}}
{{#each clients}}
<tr data-client-key="{{id}}" data-client-id="{{clientId}}" data-client-device-id="{{deviceId}}" data-client-screen-slug="{{screen_slug}}">
<tr data-table-search-row data-client-key="{{id}}" data-client-id="{{clientId}}" data-client-device-id="{{deviceId}}" data-client-screen-slug="{{screen_slug}}">
<td data-label="Client" class="client-rename-cell" title="Double-click to rename">
<div>{{#if client_name}}{{client_name}}{{else}}Unknown{{/if}}</div>
</td>
@@ -95,9 +101,10 @@
</tr>
{{/each}}
{{else}}
<tr><td colspan="{{#if (hasPermission currentUser 'clients.allow')}}7{{else}}6{{/if}}" class="empty">No connected clients yet.</td></tr>
<tr data-table-search-empty-default><td colspan="{{#if (hasPermission currentUser 'clients.allow')}}7{{else}}6{{/if}}" class="empty">No connected clients yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
{{> table-pagination pagination=pagination basePath="/clients"}}
</div>
+70 -41
View File
@@ -12,51 +12,36 @@
<h3 class="card-title">Playlist details</h3>
</div>
<div class="card-body">
<form id="playlist-edit-form" method="post" action="/playlists/{{playlist.id}}" data-async-save data-async-save-close-url="/playlists" data-async-save-new-url="/playlists/new">
<div class="row g-3 align-items-end">
<form id="playlist-edit-form" method="post" action="/playlists/{{playlist.id}}" data-async-save data-async-save-reload-on-success data-async-save-close-url="/playlists" data-async-save-new-url="/playlists/new">
<div class="row g-2 align-items-end">
<div class="col-12 col-lg-8">
<label for="playlist-name" class="form-label">Name</label>
<input id="playlist-name" name="name" class="form-control" value="{{playlist.name}}" required />
</div>
<div class="col-12 col-lg-4 d-flex align-items-lg-end justify-content-lg-start">
<div class="d-flex flex-column align-items-lg-start gap-1 w-100">
<label class="form-label d-none d-lg-block invisible">Add slide</label>
{{#if hasSlides}}
{{#if hasSelectableSlides}}
<button type="button" class="btn btn-primary w-100" id="playlist-open-slide-modal" data-bs-toggle="modal" data-bs-target="#playlist-add-slide-modal">Add slide</button>
{{else}}
<div class="text-body-secondary small text-lg-start">
{{#if playlistCanvasMismatch}}
Remove mismatched slides before adding more.
{{else if playlistCanvasSignature}}
No slides match this playlist canvas size.
{{else}}
No slides are currently available for this playlist.
{{/if}}
</div>
{{/if}}
{{else}}
<div class="text-body-secondary small text-lg-start">Create slides first.</div>
{{/if}}
</div>
</div>
</div>
</form>
<div class="row g-3 mt-1 align-items-start">
<div class="col-12">
<h4 class="h6 mb-2 text-uppercase text-muted">Add slide to playlist</h4>
{{#if hasSlides}}
{{#if hasSelectableSlides}}
<form id="playlist-add-slide-form">
<div class="row g-2 align-items-end playlist-add-slide-form">
<div class="col-12 col-md-8">
<select id="playlist-add-slide-select" class="form-select" required>
{{#each selectableSlides}}
<option value="{{id}}" data-canvas-signature="{{canvasSignature}}" {{#if isAssigned}}disabled{{/if}}>{{title}}</option>
{{/each}}
</select>
</div>
<div class="col-12 col-md-4 d-flex justify-content-md-end">
<button type="button" class="btn btn-primary w-100 w-md-auto" id="playlist-add-slide-button">Add slide</button>
</div>
</div>
</form>
<div id="playlist-add-slide-empty" class="empty mt-2 playlist-add-slide-empty is-hidden">No slides are currently available for this playlist.</div>
{{else}}
<div class="empty mb-0">
{{#if playlistCanvasMismatch}}
This playlist contains slides with different canvas sizes. Remove the mismatched items before adding more.
{{else if playlistCanvasSignature}}
No slides match this playlist canvas size.
{{else}}
No slides are currently available for this playlist.
{{/if}}
</div>
{{/if}}
{{else}}
<div class="empty mb-0">Create slides first.</div>
{{/if}}
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
<div class="btn-group playlist-actions-group" role="group" aria-label="Playlist actions">
@@ -77,6 +62,10 @@
<input class="form-check-input" type="checkbox" role="switch" id="playlist-fade-between-slides" name="fade_between_slides" value="1" form="playlist-edit-form" {{#if playlistFadeBetweenSlides}}checked{{/if}} />
<label class="form-check-label" for="playlist-fade-between-slides">Fade between slides</label>
</div>
<div class="form-check form-switch mt-3 mb-0">
<input class="form-check-input" type="checkbox" role="switch" id="playlist-skip-unavailable-rtmp" name="skip_unavailable_rtmp" value="1" form="playlist-edit-form" {{#if playlistSkipUnavailableRtmp}}checked{{/if}} />
<label class="form-check-label" for="playlist-skip-unavailable-rtmp">Skip RTMP slides when stream is unavailable</label>
</div>
</div>
</div>
</div>
@@ -110,7 +99,18 @@
</div>
</td>
<td data-label="Slide">
{{title}}
<div class="playlist-slide-cell">
<div class="playlist-slide-thumb" aria-hidden="true">
{{#if thumbnail_path}}
<img class="playlist-slide-thumb-image" src="{{thumbnail_path}}" alt="" loading="lazy" />
{{else}}
<span class="playlist-slide-thumb-placeholder"><i class="bi bi-image" aria-hidden="true"></i></span>
{{/if}}
</div>
<div class="playlist-slide-cell-content">
<span class="playlist-slide-title">{{title}}</span>
</div>
</div>
<input type="hidden" name="slide_id[]" value="{{slide_id}}" form="playlist-edit-form" />
</td>
<td data-label="Schedule">
@@ -123,7 +123,7 @@
<input type="hidden" name="schedule_days_json[]" value="{{schedule_days_json}}" form="playlist-edit-form" />
</td>
<td data-label="Duration">
<input name="duration_seconds[]" type="number" min="1" value="{{duration_seconds}}" required form="playlist-edit-form" />
<input name="duration_seconds[]" type="number" min="1" value="{{duration_seconds}}" required form="playlist-edit-form" class="form-control form-control-sm playlist-duration-input text-end" />
</td>
<td data-label="Actions">
<div class="actions playlist-item-actions">
@@ -143,5 +143,34 @@
</div>
</div>
<script type="application/json" id="playlist-add-slide-data">{{json selectableSlides}}</script>
{{#> modal-shell modalId="playlist-add-slide-modal" modalLabelId="playlist-add-slide-modal-label" modalDialogClass="modal-dialog-centered modal-dialog-scrollable modal-xl" modalBackdropStatic=true modalKeyboardDisabled=true}}
<div class="modal-header">
<div>
<h2 class="modal-title fs-5" id="playlist-add-slide-modal-label">Add slides to playlist</h2>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body playlist-slide-picker-modal-body">
<div class="playlist-slide-picker-toolbar">
<label class="form-label mb-1" for="playlist-slide-picker-search">Search</label>
<div class="input-group">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" id="playlist-slide-picker-search" placeholder="Search slide titles" autocomplete="off" />
</div>
</div>
<div class="playlist-slide-picker-empty empty is-hidden" id="playlist-slide-picker-empty">No slides match your search.</div>
<div class="playlist-slide-picker-grid" id="playlist-slide-picker-grid" aria-live="polite"></div>
</div>
<div class="modal-footer justify-content-end">
<div class="d-flex flex-wrap align-items-center justify-content-end gap-2 w-100">
<div class="text-body-secondary small text-end" id="playlist-slide-picker-selected-count-wrap"><span id="playlist-slide-picker-selected-count">0</span> selected</div>
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" id="playlist-confirm-add-slides" disabled>Add selected slides</button>
</div>
</div>
{{/modal-shell}}
<form id="delete-playlist-form" method="post" action="/playlists/{{playlist.id}}/delete" data-confirm-message="Delete this playlist?"></form>
+13 -8
View File
@@ -5,17 +5,21 @@
</div>
</div>
<div class="card card-outline card-primary">
<div class="card card-outline card-primary" data-table-search-container>
<div class="card-header">
<h3 class="card-title">Existing playlists</h3>
{{#if (hasPermission currentUser 'playlists.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/playlists/new">Create playlist</a>
<div class="card-tools d-flex flex-nowrap align-items-center gap-2">
<div class="input-group input-group-sm flex-shrink-1" style="width: min(14rem, 100%);">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" placeholder="Search playlists" aria-label="Search playlists" data-table-search />
</div>
{{/if}}
{{#if (hasPermission currentUser 'playlists.create')}}
<a class="btn btn-primary btn-sm" href="/playlists/new">Create playlist</a>
{{/if}}
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<table class="table table-striped w-100 mb-0" data-table-searchable>
<thead>
<tr>
<th>Name</th>
@@ -26,7 +30,7 @@
<tbody>
{{#if playlists.length}}
{{#each playlists}}
<tr>
<tr data-table-search-row>
<td data-label="Name">{{name}}</td>
<td data-label="Slides"><span class="chip">{{slideCount}} slide{{#unless slideCountIsOne}}s{{/unless}}</span></td>
<td data-label="Actions">
@@ -48,10 +52,11 @@
</tr>
{{/each}}
{{else}}
<tr><td colspan="3" class="empty">No playlists yet.</td></tr>
<tr data-table-search-empty-default><td colspan="3" class="empty">No playlists yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
{{> table-pagination pagination=pagination basePath="/playlists"}}
</div>
+13 -8
View File
@@ -5,17 +5,21 @@
</div>
</div>
<div class="card card-outline card-primary">
<div class="card card-outline card-primary" data-table-search-container>
<div class="card-header">
<h3 class="card-title">Existing screens</h3>
{{#if (hasPermission currentUser 'screens.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/screens/new">Add screen</a>
<div class="card-tools d-flex flex-nowrap align-items-center gap-2">
<div class="input-group input-group-sm flex-shrink-1" style="width: min(14rem, 100%);">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" placeholder="Search screens" aria-label="Search screens" data-table-search />
</div>
{{/if}}
{{#if (hasPermission currentUser 'screens.create')}}
<a class="btn btn-primary btn-sm" href="/screens/new">Add screen</a>
{{/if}}
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<table class="table table-striped w-100 mb-0" data-table-searchable>
<thead>
<tr>
<th>Name</th>
@@ -28,7 +32,7 @@
<tbody>
{{#if screens.length}}
{{#each screens}}
<tr>
<tr data-table-search-row>
<td data-label="Name">{{name}}</td>
<td data-label="Player URL"><a href="{{playerUrl slug}}" target="_blank">{{playerUrl slug}}</a></td>
<td data-label="Playlist">{{playlist_name}}</td>
@@ -58,10 +62,11 @@
</tr>
{{/each}}
{{else}}
<tr><td colspan="5" class="empty">No screens yet.</td></tr>
<tr data-table-search-empty-default><td colspan="5" class="empty">No screens yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
{{> table-pagination pagination=pagination basePath="/screens"}}
</div>
+1 -1
View File
@@ -72,5 +72,5 @@
{{/if}}
<textarea id="slide-editor-data" hidden>{{json slideEditorData}}</textarea>
<script type="module" src="/assets/js/slides/slide-form.js?v=20260724"></script>
<script type="module" src="/assets/js/slides/slide-form.js?v=20260725"></script>
+29 -8
View File
@@ -5,19 +5,24 @@
</div>
</div>
<div class="card card-outline card-primary">
<div class="card card-outline card-primary" data-table-search-container>
<div class="card-header">
<h3 class="card-title">Existing slides</h3>
{{#if (hasPermission currentUser 'slides.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/slides/new">Create slide</a>
<div class="card-tools d-flex flex-nowrap align-items-center gap-2">
<div class="input-group input-group-sm flex-shrink-1" style="width: min(14rem, 100%);">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" placeholder="Search slides" aria-label="Search slides" data-table-search />
</div>
{{/if}}
{{#if (hasPermission currentUser 'slides.create')}}
<a class="btn btn-primary btn-sm" href="/slides/new">Create slide</a>
{{/if}}
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<table class="table table-striped w-100 mb-0" data-table-searchable>
<thead>
<tr>
<th>Thumbnail</th>
<th>Title</th>
<th>Template</th>
<th>Actions</th>
@@ -26,7 +31,16 @@
<tbody>
{{#if slides.length}}
{{#each slides}}
<tr>
<tr data-table-search-row data-search-text="{{title}} {{template_name}}">
<td data-label="Thumbnail">
<div class="playlist-slide-thumb" aria-hidden="true">
{{#if thumbnail_path}}
<img class="playlist-slide-thumb-image" src="{{thumbnail_path}}" alt="" loading="lazy" />
{{else}}
<span class="playlist-slide-thumb-placeholder"><i class="bi bi-image" aria-hidden="true"></i></span>
{{/if}}
</div>
</td>
<td data-label="Title">{{title}}</td>
<td data-label="Template">{{template_name}}</td>
<td data-label="Actions">
@@ -48,10 +62,17 @@
</tr>
{{/each}}
{{else}}
<tr><td colspan="3" class="empty">No slides yet.</td></tr>
<tr data-table-search-empty-default><td colspan="4" class="empty">No slides yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
{{> table-pagination pagination=pagination basePath="/slides"}}
</div>
<script>
if (typeof window.attachSlideThumbFallbacks === 'function') {
window.attachSlideThumbFallbacks(document);
}
</script>
+16 -20
View File
@@ -106,28 +106,24 @@
</div>
</form>
<div class="modal fade" id="region-add-modal" tabindex="-1" aria-labelledby="region-add-modal-label" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="region-add-modal-label">Add region</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="text-body-secondary mb-3">Choose the type of region you want to add to this template.</p>
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="html">HTML</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="image">Image</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="api">API</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="rtmp">RTMP</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="rss">RSS</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="text">Text</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="webpage">Webpage</button>
</div>
</div>
{{#> modal-shell modalId="region-add-modal" modalLabelId="region-add-modal-label" modalDialogClass="modal-dialog-centered"}}
<div class="modal-header">
<h2 class="modal-title fs-5" id="region-add-modal-label">Add region</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="text-body-secondary mb-3">Choose the type of region you want to add to this template.</p>
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="html">HTML</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="image">Image</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="api">API</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="rtmp">RTMP</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="rss">RSS</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="text">Text</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="webpage">Webpage</button>
</div>
</div>
</div>
{{/modal-shell}}
<template id="region-card-template">
<div class="card card-outline card-secondary admin-form-card region-item mb-3">
+16 -20
View File
@@ -113,28 +113,24 @@
<form id="delete-template-form" method="post" action="/templates/{{template.id}}/delete" data-confirm-message="Delete this template?"></form>
<div class="modal fade" id="region-add-modal" tabindex="-1" aria-labelledby="region-add-modal-label" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title fs-5" id="region-add-modal-label">Add region</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="text-body-secondary mb-3">Choose the type of region you want to add to this template.</p>
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="html">HTML</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="image">Image</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="api">API</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="rtmp">RTMP</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="rss">RSS</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="text">Text</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="webpage">Webpage</button>
</div>
</div>
{{#> modal-shell modalId="region-add-modal" modalLabelId="region-add-modal-label" modalDialogClass="modal-dialog-centered"}}
<div class="modal-header">
<h2 class="modal-title fs-5" id="region-add-modal-label">Add region</h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="text-body-secondary mb-3">Choose the type of region you want to add to this template.</p>
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="html">HTML</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="image">Image</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="api">API</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="rtmp">RTMP</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="rss">RSS</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="text">Text</button>
<button type="button" class="btn btn-outline-secondary text-start" data-add-region-type="webpage">Webpage</button>
</div>
</div>
</div>
{{/modal-shell}}
<template id="region-card-template">
<div class="card card-outline card-secondary admin-form-card region-item mb-3">
+13 -8
View File
@@ -5,17 +5,21 @@
</div>
</div>
<div class="card card-outline card-primary">
<div class="card card-outline card-primary" data-table-search-container>
<div class="card-header">
<h3 class="card-title">Existing templates</h3>
{{#if (hasPermission currentUser 'templates.create')}}
<div class="card-tools">
<a class="btn btn-primary btn-sm" href="/templates/new">Create template</a>
<div class="card-tools d-flex flex-nowrap align-items-center gap-2">
<div class="input-group input-group-sm flex-shrink-1" style="width: min(14rem, 100%);">
<span class="input-group-text" aria-hidden="true"><i class="bi bi-search"></i></span>
<input type="search" class="form-control" placeholder="Search templates" aria-label="Search templates" data-table-search />
</div>
{{/if}}
{{#if (hasPermission currentUser 'templates.create')}}
<a class="btn btn-primary btn-sm" href="/templates/new">Create template</a>
{{/if}}
</div>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<table class="table table-striped w-100 mb-0" data-table-searchable>
<thead>
<tr>
<th>Name</th>
@@ -27,7 +31,7 @@
<tbody>
{{#if templates.length}}
{{#each templates}}
<tr>
<tr data-table-search-row>
<td data-label="Name">{{name}}</td>
<td data-label="Canvas">{{canvas_size_width}}x{{canvas_size_height}}</td>
<td data-label="Regions">{{regionCount}}</td>
@@ -50,10 +54,11 @@
</tr>
{{/each}}
{{else}}
<tr><td colspan="4" class="empty">No templates yet.</td></tr>
<tr data-table-search-empty-default><td colspan="4" class="empty">No templates yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
{{> table-pagination pagination=pagination basePath="/templates"}}
</div>