Save worktree changes

This commit is contained in:
2026-07-25 02:29:19 +01:00
parent 8d3b7d557b
commit db9d718cd8
170 changed files with 11719 additions and 3414 deletions
@@ -0,0 +1,37 @@
<div class="page-header">
<div>
<h2>Create canvas size</h2>
<p>Add a reusable canvas dimension for templates.</p>
</div>
</div>
<div class="card card-outline card-primary admin-form-card">
<div class="card-header">
<h3 class="card-title">Canvas size details</h3>
</div>
<form id="canvas-size-form" method="post" action="/canvas-sizes">
<div class="card-body">
<div class="mb-3">
<label for="canvas-size-name" class="form-label">Name</label>
<input id="canvas-size-name" name="name" class="form-control" value="{{canvasSize.name}}" required />
</div>
<div class="row g-3">
<div class="col-12 col-md-6">
<label for="canvas-size-width" class="form-label">Width</label>
<input id="canvas-size-width" name="width" type="number" min="1" class="form-control" value="{{canvasSize.width}}" required />
</div>
<div class="col-12 col-md-6">
<label for="canvas-size-height" class="form-label">Height</label>
<input id="canvas-size-height" name="height" type="number" min="1" class="form-control" value="{{canvasSize.height}}" required />
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
<div class="btn-group" role="group" aria-label="Canvas size actions">
{{{saveActionButtons formId="canvas-size-form" saveLabel="Save" showSaveAndClose=false showSaveAndNew=false}}}
<a class="btn btn-warning" href="/canvas-sizes" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
</div>
</div>
</form>
</div>
@@ -0,0 +1,40 @@
<div class="page-header">
<div>
<h2>Edit canvas - {{canvasSize.name}}</h2>
<p>Update the stored canvas dimensions.</p>
</div>
</div>
<div class="card card-outline card-primary admin-form-card">
<div class="card-header">
<h3 class="card-title">Canvas size details</h3>
</div>
<form id="canvas-size-form" method="post" action="/canvas-sizes/{{canvasSize.id}}" data-async-save data-async-save-close-url="/canvas-sizes" data-async-save-new-url="/canvas-sizes/new">
<div class="card-body">
<div class="mb-3">
<label for="canvas-size-name" class="form-label">Name</label>
<input id="canvas-size-name" name="name" class="form-control" value="{{canvasSize.name}}" required />
</div>
<div class="row g-3">
<div class="col-12 col-md-6">
<label for="canvas-size-width" class="form-label">Width</label>
<input id="canvas-size-width" name="width" type="number" min="1" class="form-control" value="{{canvasSize.width}}" required />
</div>
<div class="col-12 col-md-6">
<label for="canvas-size-height" class="form-label">Height</label>
<input id="canvas-size-height" name="height" type="number" min="1" class="form-control" value="{{canvasSize.height}}" required />
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
<div class="btn-group" role="group" aria-label="Canvas size actions">
{{{saveActionButtons formId="canvas-size-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
<a class="btn btn-warning" href="/canvas-sizes" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
<button type="submit" class="btn btn-danger" form="delete-canvas-size-form">Delete</button>
</div>
</div>
</form>
</div>
<form id="delete-canvas-size-form" method="post" action="/canvas-sizes/{{canvasSize.id}}/delete" data-confirm-message="Delete this canvas size?"></form>
@@ -0,0 +1,58 @@
<div class="page-header">
<div>
<h2>Canvas sizes</h2>
<p>Keep template dimensions standardized across the system.</p>
</div>
</div>
<div class="card card-outline card-primary">
<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>
{{/if}}
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<thead>
<tr>
<th>Name</th>
<th>Dimensions</th>
<th>Templates</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{#if canvasSizes.length}}
{{#each canvasSizes}}
<tr>
<td data-label="Name">{{name}}</td>
<td data-label="Dimensions">{{width}}x{{height}}</td>
<td data-label="Templates">{{templateCount}}</td>
<td data-label="Actions">
{{#if (anyPermission ../currentUser 'canvas-sizes.update' 'canvas-sizes.delete')}}
<div class="actions">
{{#if (hasPermission ../currentUser 'canvas-sizes.update')}}
<a class="btn btn-sm btn-primary" href="/canvas-sizes/{{id}}/edit">Edit</a>
{{/if}}
{{#if (hasPermission ../currentUser 'canvas-sizes.delete')}}
<form class="inline-form" method="post" action="/canvas-sizes/{{id}}/delete" data-confirm-message="Delete this canvas size?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
{{/if}}
</div>
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
</tr>
{{/each}}
{{else}}
<tr><td colspan="4" class="empty">No canvas sizes yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
+103
View File
@@ -0,0 +1,103 @@
<div class="page-header">
<div>
<h2>Connected clients</h2>
<p>Inspect live player connections and send commands to individual clients.</p>
</div>
</div>
<div class="card card-outline card-primary">
<div class="card-header">
<h3 class="card-title">Active connections</h3>
</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}}">
<thead>
<tr>
<th>Client</th>
<th>Current Screen</th>
<th>Current Slide</th>
<th>IP</th>
<th>Viewport</th>
<th>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-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>
<td data-label="Current Screen">
<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="IP">{{#if clientIp}}{{clientIp}}{{else}}<span class="empty">Unknown</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}}" />
<button type="submit" class="btn btn-sm btn-danger">Reload</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}}" />
<button type="submit" class="btn btn-sm btn-warning" aria-label="Previous slide">◀</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}}" />
<button type="submit" class="btn btn-sm btn-warning" aria-label="Next slide">▶</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}}" />
<button type="submit" class="btn btn-sm btn-info">
<i class="bi bi-pause-fill me-1" aria-hidden="false"></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}}" />
<button type="submit" class="btn btn-sm {{#if blackout}}btn-success{{else}}btn-secondary{{/if}}">
<i class="bi bi-eye-slash me-1" aria-hidden="false"></i>Blackout
</button>
</form>
</div>
</td>
{{/if}}
</tr>
{{/each}}
{{else}}
<tr><td colspan="{{#if (hasPermission currentUser 'clients.allow')}}7{{else}}6{{/if}}" class="empty">No connected clients yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
</div>
+138
View File
@@ -0,0 +1,138 @@
<div class="page-header">
<div>
<h2>Dashboard</h2>
<p>Monitor screens and quick actions from one place.</p>
</div>
</div>
<section class="dashboard-hero card card-outline card-primary mb-4">
<div class="card-body dashboard-hero-body">
<div>
<span class="dashboard-hero-kicker">Live overview</span>
<h3 class="dashboard-hero-title">Keep the control surface focused on live state and actions.</h3>
<p class="dashboard-hero-copy">Use the cards below for the current totals, then jump to the screens table when you need a deeper look at playback and connection state.</p>
</div>
<div class="dashboard-hero-stats">
{{#if (hasPermission currentUser "playlists.read")}}
<div class="dashboard-hero-stat">
<span class="dashboard-hero-stat-value">{{playlists.length}}</span>
<span class="dashboard-hero-stat-label">playlists</span>
</div>
{{/if}}
{{#if (hasPermission currentUser "screens.read")}}
<div class="dashboard-hero-stat">
<span class="dashboard-hero-stat-value">{{screens.length}}</span>
<span class="dashboard-hero-stat-label">screens</span>
</div>
{{/if}}
{{#if (hasPermission currentUser "clients.read")}}
<div class="dashboard-hero-stat">
<span class="dashboard-hero-stat-value">{{connectedClientsCount}}</span>
<span class="dashboard-hero-stat-label">clients</span>
</div>
{{/if}}
</div>
</div>
</section>
{{#if (hasPermission currentUser "dashboard.allow")}}
<div class="row">
<div class="col-12">
<div class="card mb-4 card-outline card-primary dashboard-actions-card">
<div class="card-header">
<div class="dashboard-card-heading">
<h3 class="card-title">Quick actions</h3>
<p class="dashboard-card-subtitle">Send global commands without
leaving the overview.</p>
</div>
</div>
<div class="card-body">
<div class="dashboard-action-grid">
<form
method="post"
action="/commands"
class="dashboard-action-form"
data-confirm-message="Reload all connected clients?"
data-async-command
>
<input type="hidden" name="command" value="reload" />
<button
type="submit"
class="btn btn-danger dashboard-action-button"
id="dashboard-reload-all-button"
><i class="bi bi-arrow-repeat me-1" aria-hidden="true"></i>Reload all clients</button>
<span class="dashboard-action-help">Restart every connected player page.</span>
</form>
<form
method="post"
action="/commands"
class="dashboard-action-form"
data-confirm-message="Blackout all connected clients?"
data-async-command
>
<input type="hidden" name="command" value="blackout" />
<input type="hidden" name="blackout" value="true" />
<button
type="submit"
class="btn btn-secondary dashboard-action-button"
id="dashboard-blackout-all-button"
><i class="bi bi-eye-slash me-1" aria-hidden="false"></i>Blackout
all clients</button>
<span class="dashboard-action-help">Blank active players
immediately.</span>
</form>
</div>
</div>
</div>
</div>
</div>
{{/if}}
{{#if (hasPermission currentUser "screens.read")}}
<div class="row">
<div class="col-12">
<div class="card card-outline card-secondary dashboard-table-card">
<div class="card-header">
<div class="dashboard-card-heading">
<h3 class="card-title">Current screens</h3>
<p class="dashboard-card-subtitle">Player URL, playlist assignment,
and live connection count.</p>
</div>
</div>
<div class="card-body table-responsive p-0">
<table
id="dashboard-screens-table"
class="table table-striped w-100 mb-0"
>
<thead><tr><th>Name</th><th>Player URL</th><th>Playlist</th><th
>Connected clients</th></tr></thead>
<tbody>
{{#if screens.length}}
{{#each screens}}
<tr data-client-key="{{id}}">
<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>
<td data-label="Connected clients">
{{#if player_connection_count}}
<div class="connection-count">{{player_connection_count}}
connected</div>
{{else}}
<span class="empty">No clients connected.</span>
{{/if}}
</td>
</tr>
{{/each}}
{{else}}
<tr><td colspan="4" class="empty">No screens yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
</div>
</div>
</div>
{{/if}}
+27
View File
@@ -0,0 +1,27 @@
<div class="page-header">
<div>
<h2>Create playlist</h2>
<p>Name the playlist and start adding slides.</p>
</div>
</div>
<div class="card card-outline card-primary admin-form-card">
<div class="card-header">
<h3 class="card-title">Playlist details</h3>
</div>
<form id="playlist-edit-form" method="post" action="/playlists" data-async-save data-async-save-new-url="/playlists/new">
<div class="card-body">
<div class="mb-0">
<label for="playlist-name" class="form-label">Name</label>
<input id="playlist-name" name="name" class="form-control" placeholder="Lobby loop" required />
</div>
</div>
<div class="card-footer d-flex justify-content-end">
<div class="btn-group" role="group" aria-label="Playlist actions">
{{{saveActionButtons formId="playlist-edit-form" saveLabel="Save" showSaveAndClose=false showSaveAndNew=false}}}
<a class="btn btn-warning" href="/playlists" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
</div>
</div>
</form>
</div>
+147
View File
@@ -0,0 +1,147 @@
<div class="page-header">
<div>
<h2>Edit playlist - {{playlist.name}}</h2>
<p>Adjust the playlist details, slide order, and scheduling.</p>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-12 col-lg-8">
<div class="card card-outline card-primary mb-0">
<div class="card-header">
<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">
<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>
</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">
{{{saveActionButtons formId="playlist-edit-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
<a class="btn btn-warning" href="/playlists" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
<button type="submit" class="btn btn-danger" form="delete-playlist-form">Delete</button>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-4">
<div class="card card-outline card-light h-100 mb-0">
<div class="card-header">
<h4 class="card-title">Options</h4>
</div>
<div class="card-body py-3">
<div class="form-check form-switch mb-0">
<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>
</div>
</div>
</div>
<div class="row g-4 mb-4">
<div class="col-12">
<div class="card card-outline card-secondary mb-0">
<div class="card-header">
<h3 class="card-title">Playlist items</h3>
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<thead><tr><th>Order</th><th>Slide</th><th>Schedule</th><th>Duration</th><th>Actions</th></tr></thead>
<tbody id="playlist-items-body" data-playlist-id="{{playlist.id}}">
{{#if playlistSlides.length}}
{{#each playlistSlides}}
<tr data-playlist-slide-row data-row-key="existing-{{id}}" data-slide-id="{{slide_id}}" data-canvas-signature="{{canvasSignature}}">
<td class="playlist-order-cell" data-label="Order">
<div class="playlist-order-cell-inner">
<button type="button" class="playlist-drag-handle btn btn-link p-0 text-body-secondary" data-playlist-drag-handle aria-label="Drag to reorder" title="Drag to reorder">
<span class="playlist-drag-handle-icon" aria-hidden="true">
<svg class="playlist-drag-handle-svg" viewBox="0 0 24 32" focusable="false" aria-hidden="true">
<polygon points="12,2 20,9 4,9"></polygon>
<rect x="4" y="14" width="16" height="4" rx="2"></rect>
<polygon points="4,23 20,23 12,30"></polygon>
</svg>
</span>
</button>
<span class="playlist-order-number">{{displayOrder}}</span>
</div>
</td>
<td data-label="Slide">
{{title}}
<input type="hidden" name="slide_id[]" value="{{slide_id}}" form="playlist-edit-form" />
</td>
<td data-label="Schedule">
<div class="playlist-schedule-summary">{{scheduleSummary}}</div>
<input type="hidden" name="schedule_mode[]" value="{{schedule_mode}}" form="playlist-edit-form" />
<input type="hidden" name="schedule_start_datetime[]" value="{{schedule_start_datetime}}" form="playlist-edit-form" />
<input type="hidden" name="schedule_end_datetime[]" value="{{schedule_end_datetime}}" form="playlist-edit-form" />
<input type="hidden" name="schedule_start_time[]" value="{{schedule_start_time}}" form="playlist-edit-form" />
<input type="hidden" name="schedule_end_time[]" value="{{schedule_end_time}}" form="playlist-edit-form" />
<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" />
</td>
<td data-label="Actions">
<div class="actions playlist-item-actions">
<button type="button" class="btn btn-sm btn-primary" data-schedule-config="/playlists/{{../playlist.id}}/slides/{{id}}/config" data-schedule-config-row="existing-{{id}}">Schedule</button>
<button type="button" class="btn btn-sm btn-danger" data-playlist-remove-row>Remove</button>
</div>
</td>
</tr>
{{/each}}
{{else}}
<tr class="playlist-empty-row"><td colspan="5" class="empty">No slides assigned yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
</div>
</div>
</div>
<form id="delete-playlist-form" method="post" action="/playlists/{{playlist.id}}/delete" data-confirm-message="Delete this playlist?"></form>
+57
View File
@@ -0,0 +1,57 @@
<div class="page-header">
<div>
<h2>Playlists</h2>
<p>Organize slide rotations and manage scheduled playback.</p>
</div>
</div>
<div class="card card-outline card-primary">
<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>
{{/if}}
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<thead>
<tr>
<th>Name</th>
<th>Slides</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{#if playlists.length}}
{{#each playlists}}
<tr>
<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">
{{#if (anyPermission ../currentUser 'playlists.update' 'playlists.delete')}}
<div class="actions">
{{#if (hasPermission ../currentUser 'playlists.update')}}
<a class="btn btn-sm btn-primary" href="/playlists/{{id}}/edit">Edit</a>
{{/if}}
{{#if (hasPermission ../currentUser 'playlists.delete')}}
<form class="inline-form" method="post" action="/playlists/{{id}}/delete" data-confirm-message="Delete this playlist?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
{{/if}}
</div>
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
</tr>
{{/each}}
{{else}}
<tr><td colspan="3" class="empty">No playlists yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
</div>
@@ -0,0 +1,69 @@
<div class="card card-outline card-primary mb-0 slide-schedule-card">
<div class="card-header">
<h3 class="card-title">Slide schedule</h3>
</div>
<form method="post" action="/playlists/{{playlist.id}}/slides/{{playlistSlide.id}}/config">
<div class="card-body">
<input type="hidden" name="playlist_slide_id" value="{{playlistSlide.id}}" />
<input type="hidden" name="row_key" value="{{rowKey}}" />
<div class="mb-3">
<label for="schedule-mode-select" class="form-label">Schedule mode</label>
<select name="schedule_mode" id="schedule-mode-select" class="form-select">
<option value="always" {{#if isAlwaysMode}}selected{{/if}}>Always visible</option>
<option value="dates" {{#if isDatesMode}}selected{{/if}}>Between two datetimes</option>
<option value="times" {{#if isTimesMode}}selected{{/if}}>Between times on selected days</option>
</select>
</div>
<div class="card card-outline card-primary mb-3 schedule-panel {{#unless isDatesMode}}is-hidden{{/unless}}" id="schedule-dates-panel">
<div class="card-header py-2">
<h4 class="h6 mb-0">Date range</h4>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12 col-md-6">
<label for="schedule-start-datetime" class="form-label">Start datetime</label>
<input id="schedule-start-datetime" type="datetime-local" name="schedule_start_datetime" class="form-control" value="{{startDateTimeValue}}" />
</div>
<div class="col-12 col-md-6">
<label for="schedule-end-datetime" class="form-label">End datetime</label>
<input id="schedule-end-datetime" type="datetime-local" name="schedule_end_datetime" class="form-control" value="{{endDateTimeValue}}" />
</div>
</div>
</div>
</div>
<div class="card card-outline card-secondary mb-0 schedule-panel {{#unless isTimesMode}}is-hidden{{/unless}}" id="schedule-times-panel">
<div class="card-header py-2">
<h4 class="h6 mb-0">Time range</h4>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12 col-md-6">
<label for="schedule-start-time" class="form-label">Start time</label>
<input id="schedule-start-time" type="time" name="schedule_start_time" class="form-control" value="{{startTimeValue}}" />
</div>
<div class="col-12 col-md-6">
<label for="schedule-end-time" class="form-label">End time</label>
<input id="schedule-end-time" type="time" name="schedule_end_time" class="form-control" value="{{endTimeValue}}" />
</div>
</div>
<div class="btn-group btn-group-sm w-100 schedule-day-grid mt-3" role="group" aria-label="Schedule days">
{{#each dayOptions}}
<input class="btn-check" type="checkbox" id="schedule-day-{{value}}" name="schedule_days" value="{{value}}" autocomplete="off" {{#if checked}}checked{{/if}} />
<label class="btn btn-outline-primary flex-fill schedule-day-button" for="schedule-day-{{value}}">{{label}}</label>
{{/each}}
</div>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
<div class="btn-group" role="group" aria-label="Slide schedule actions">
<button type="submit" class="btn btn-primary">Save schedule</button>
<button type="button" class="btn btn-warning" data-confirm-unsaved="You have unsaved changes. Leave this page?" onclick="window.closeScheduleModal && window.closeScheduleModal();">Cancel</button>
</div>
</div>
</form>
</div>
+42
View File
@@ -0,0 +1,42 @@
<div class="page-header">
<div>
<h2>Add screen</h2>
<p>Register a player endpoint and connect it to a playlist.</p>
</div>
</div>
<div class="card card-outline card-primary admin-form-card">
<div class="card-header">
<h3 class="card-title">Screen details</h3>
</div>
<form id="screen-edit-form" method="post" action="/screens" data-async-save data-async-save-new-url="/screens/new">
<div class="card-body">
<div class="row g-3">
<div class="col-12 col-md-6">
<label for="screen-name" class="form-label">Name</label>
<input id="screen-name" name="name" class="form-control" placeholder="Front desk TV" required />
</div>
<div class="col-12 col-md-6">
<label for="screen-slug" class="form-label">Slug</label>
<input id="screen-slug" name="slug" class="form-control" placeholder="front-desk-tv" />
</div>
<div class="col-12">
<label for="screen-playlist" class="form-label">Playlist</label>
<select id="screen-playlist" name="playlist_id" class="form-select">
<option value="">-- not assigned --</option>
{{#each playlists}}
<option value="{{id}}">{{name}}</option>
{{/each}}
</select>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
<div class="btn-group" role="group" aria-label="Screen actions">
{{{saveActionButtons formId="screen-edit-form" saveLabel="Save" showSaveAndClose=false showSaveAndNew=false}}}
<a class="btn btn-warning" href="/screens" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
</div>
</div>
</form>
</div>
+57
View File
@@ -0,0 +1,57 @@
<div class="page-header">
<div>
<h2>Edit screen - {{screen.name}}</h2>
<p>Update the display name and assigned playlist.</p>
</div>
</div>
<div class="row g-4">
<div class="col-12 col-xl-8">
<div class="card card-outline card-primary admin-form-card h-100">
<div class="card-header">
<h3 class="card-title">Screen details</h3>
</div>
<form id="screen-edit-form" method="post" action="/screens/{{screen.id}}" data-async-save data-async-save-close-url="/screens" data-async-save-new-url="/screens/new">
<div class="card-body">
<div class="row g-3">
<div class="col-12 col-md-6">
<label for="screen-name" class="form-label">Name</label>
<input id="screen-name" name="name" class="form-control" value="{{screen.name}}" required />
</div>
<div class="col-12 col-md-6">
<label for="screen-slug" class="form-label">Slug</label>
<input id="screen-slug" name="slug" class="form-control" value="{{screen.slug}}" />
</div>
<div class="col-12">
<label for="screen-playlist" class="form-label">Playlist</label>
<select id="screen-playlist" name="playlist_id" class="form-select">
<option value="">-- not assigned --</option>
{{#each playlists}}
<option value="{{id}}" {{#if (eq ../screen.playlist_id id)}}selected{{/if}}>{{name}}</option>
{{/each}}
</select>
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
<div class="btn-group screen-actions-group" role="group" aria-label="Screen actions">
{{{saveActionButtons formId="screen-edit-form" saveLabel="Save" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
<a class="btn btn-warning" href="/screens" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
<button type="submit" class="btn btn-danger" form="screen-delete-form">Delete</button>
</div>
</div>
</form>
</div>
</div>
<div class="col-12 col-xl-4">
<div class="card card-outline card-secondary admin-form-card h-100">
<div class="card-header">
<h3 class="card-title">Player URL</h3>
</div>
<div class="card-body">
<p class="mb-0"><a href="{{playerUrl screen.slug}}" target="_blank">{{playerUrl screen.slug}}</a></p>
</div>
</div>
</div>
</div>
+67
View File
@@ -0,0 +1,67 @@
<div class="page-header">
<div>
<h2>Screens</h2>
<p>Register display endpoints and keep playlist assignments in sync.</p>
</div>
</div>
<div class="card card-outline card-primary">
<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>
{{/if}}
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<thead>
<tr>
<th>Name</th>
<th>Player URL</th>
<th>Playlist</th>
<th>Connected clients</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{#if screens.length}}
{{#each screens}}
<tr>
<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>
<td data-label="Connected clients">
{{#if player_connection_count}}
<div class="connection-count">{{player_connection_count}} connected</div>
{{else}}
<span class="empty">No clients connected.</span>
{{/if}}
</td>
<td data-label="Actions">
{{#if (anyPermission ../currentUser 'screens.update' 'screens.delete')}}
<div class="actions">
{{#if (hasPermission ../currentUser 'screens.update')}}
<a class="btn btn-sm btn-primary" href="/screens/{{id}}/edit">Edit</a>
{{/if}}
{{#if (hasPermission ../currentUser 'screens.delete')}}
<form class="inline-form" method="post" action="/screens/{{id}}/delete" data-confirm-message="Delete this screen?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
{{/if}}
</div>
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
</tr>
{{/each}}
{{else}}
<tr><td colspan="5" class="empty">No screens yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
</div>
+76
View File
@@ -0,0 +1,76 @@
<div class="page-header">
<div>
<h2>{{#if isEdit}}Edit slide - {{slide.title}}{{else}}Create slide{{/if}}</h2>
<p>Build slide content and preview it against the selected template.</p>
</div>
</div>
<div class="slide-editor-shell">
<form method="post" action="{{action}}" enctype="multipart/form-data" id="slide-form" class="slide-form-stack" data-async-save-close-url="/slides" data-async-save-new-url="/slides/new">
<div class="card card-outline card-primary admin-form-card slide-template-card">
<div class="card-header">
<h3 class="card-title">Slide details</h3>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-6">
<label for="slide-title" class="form-label">Title</label>
<input id="slide-title" name="title" class="form-control" value="{{slide.title}}" required />
</div>
<div class="col-6">
<label for="template-select" class="form-label">Template</label>
<select name="template_id" id="template-select" class="form-select">
<option value="">-- none --</option>
{{#each templates}}
<option value="{{id}}" {{#if (eq ../slide.template_id id)}}selected{{/if}}>{{name}}</option>
{{/each}}
</select>
</div>
</div>
</div>
</div>
<div id="template-fields"></div>
</form>
<div class="slide-editor-sidebar">
<div class="card card-outline card-primary admin-form-card mb-3">
<div class="card-body d-grid gap-2">
<div class="btn-group template-actions-group slide-actions-group" role="group" aria-label="Slide actions">
{{{saveActionButtons formId="slide-form" saveLabel=saveLabel saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
<a class="btn btn-warning" href="/slides" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
{{#if isEdit}}
<button type="submit" class="btn btn-danger" form="delete-slide-form">Delete</button>
{{/if}}
</div>
</div>
</div>
<aside class="card card-outline card-secondary admin-form-card slide-preview-card">
<div class="card-header d-flex align-items-center justify-content-between gap-2">
<div class="slide-preview-header">
<h3>Preview</h3>
</div>
<span class="chip slide-preview-dimensions-chip" id="slide-preview-meta">--</span>
</div>
<div class="card-body slide-preview-scroll">
<div class="slide-preview-stage" id="slide-preview-stage">
<div class="slide-preview-canvas" id="slide-preview-canvas">
<img id="slide-preview-background" class="slide-preview-background" alt="" style="display:none;" />
<div class="slide-preview-empty" id="slide-preview-empty">Select a template to preview the slide.</div>
<div class="slide-preview-overlay" id="slide-preview-overlay"></div>
</div>
</div>
<button type="button" class="btn btn-outline-secondary w-100 mt-3" id="open-preview-popup">Open popup preview</button>
</div>
</aside>
</div>
</div>
{{#if isEdit}}
<form id="delete-slide-form" method="post" action="/slides/{{slide.id}}/delete" data-confirm-message="Delete this slide?"></form>
{{/if}}
<textarea id="slide-editor-data" hidden>{{json slideEditorData}}</textarea>
<script type="module" src="/assets/js/slides/slide-form.js?v=20260724"></script>
+57
View File
@@ -0,0 +1,57 @@
<div class="page-header">
<div>
<h2>Slides</h2>
<p>Manage individual slide content and template assignments.</p>
</div>
</div>
<div class="card card-outline card-primary">
<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>
{{/if}}
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<thead>
<tr>
<th>Title</th>
<th>Template</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{#if slides.length}}
{{#each slides}}
<tr>
<td data-label="Title">{{title}}</td>
<td data-label="Template">{{template_name}}</td>
<td data-label="Actions">
{{#if (anyPermission ../currentUser 'slides.update' 'slides.delete')}}
<div class="actions">
{{#if (hasPermission ../currentUser 'slides.update')}}
<a class="btn btn-sm btn-primary" href="/slides/{{id}}/edit">Edit</a>
{{/if}}
{{#if (hasPermission ../currentUser 'slides.delete')}}
<form class="inline-form" method="post" action="/slides/{{id}}/delete" data-confirm-message="Delete this slide?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
{{/if}}
</div>
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
</tr>
{{/each}}
{{else}}
<tr><td colspan="3" class="empty">No slides yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
</div>
+159
View File
@@ -0,0 +1,159 @@
<div class="page-header">
<div>
<h2>Create template</h2>
<p>Define a canvas, upload a background, and place content regions.</p>
</div>
</div>
<form method="post" action="/templates" enctype="multipart/form-data" id="template-form">
<input type="hidden" name="canvas_size_id" id="canvas-size-id" value="{{template.canvas_size_id}}" />
<input type="hidden" name="canvas_width" id="canvas-width" value="{{template.canvas_size_width}}" />
<input type="hidden" name="canvas_height" id="canvas-height" value="{{template.canvas_size_height}}" />
<input type="hidden" id="canvas-size-summary" />
<input type="hidden" name="existing_background_image_path" value="{{template.background_image_path}}" />
<input type="hidden" name="regions_json" id="regions-json" value="" />
<div class="template-designer-layout">
<div class="card card-outline card-secondary admin-form-card mb-3">
<div class="card-header">
<h3 class="card-title">Designer</h3>
</div>
<div class="card-body">
<div class="template-designer-main">
<div class="designer-stage" id="designer-stage" style="aspect-ratio: {{template.canvas_size_width}} / {{template.canvas_size_height}};">
<img id="background-preview" src="{{template.background_image_path}}" alt="" style="{{#if template.background_image_path}}{{else}}display:none;{{/if}}" />
<div class="empty-bg" id="background-empty" style="{{#if template.background_image_path}}display:none;{{/if}}">No background image selected</div>
<div class="designer-overlay" id="designer-overlay"></div>
</div>
</div>
</div>
</div>
<div class="template-designer-sidebar">
<div class="card card-outline card-primary admin-form-card">
<div class="card-body d-grid gap-2">
<div class="btn-group template-actions-group" role="group" aria-label="Template actions">
{{{saveActionButtons formId="template-form" saveLabel="Save" saveButtonClass="btn btn-success template-save-button" showSaveAndClose=false showSaveAndNew=false}}}
<a class="btn btn-warning" href="/templates" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
</div>
</div>
</div>
<div class="card card-outline card-secondary admin-form-card region-info-card mb-3">
<div class="card-header">
<div>
<h3 class="card-title">Region information</h3>
</div>
</div>
<div class="card-body">
<button type="button" class="btn btn-outline-secondary w-100" id="add-region-button" data-bs-toggle="modal" data-bs-target="#region-add-modal">Add region</button>
<div class="region-selector-block mt-4">
<label for="region-select" class="form-label">Select region</label>
<select id="region-select" class="form-select region-select"></select>
</div>
<div id="region-list" class="region-list mt-3"></div>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4 align-items-stretch">
<div class="col-12 col-xl-4">
<div class="card card-outline card-primary admin-form-card h-100 template-details-card">
<div class="card-header">
<h3 class="card-title">Template details</h3>
</div>
<div class="card-body">
<div class="row g-3 template-meta-row">
<div class="col-12">
<label for="template-name" class="form-label">Name</label>
<input name="name" id="template-name" class="form-control" value="{{template.name}}" required />
</div>
<div class="col-12">
<label for="canvas-size-select" class="form-label">Canvas size</label>
<select id="canvas-size-select" class="form-select" data-lock-on-existing-template="false">
{{#unless template.canvas_size_id}}
<option value="" data-width="{{template.canvas_size_width}}" data-height="{{template.canvas_size_height}}" selected>{{template.canvas_size_label}}</option>
{{/unless}}
{{#if canvasSizes.length}}
{{#each canvasSizes}}
<option value="{{id}}" data-width="{{width}}" data-height="{{height}}" {{#if (eq ../template.canvas_size_id id)}}selected{{/if}}>{{name}} ({{width}}x{{height}})</option>
{{/each}}
{{/if}}
</select>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-8">
<div class="card card-outline card-primary admin-form-card h-100 template-options-card">
<div class="card-header">
<h3 class="card-title">Options</h3>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12 col-md-6">
<label for="background-image" class="form-label">Background image</label>
<input type="file" name="background_image" id="background-image" class="form-control" accept="image/*" />
</div>
<div class="col-12 col-md-6">
<label for="background-color" class="form-label">Background colour</label>
<input type="color" name="background_color" id="background-color" class="form-control form-control-color w-100" value="{{#if template.background_color}}{{template.background_color}}{{else}}#111111{{/if}}" title="Choose a background colour" />
</div>
</div>
</div>
</div>
</div>
</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>
</div>
</div>
</div>
<template id="region-card-template">
<div class="card card-outline card-secondary admin-form-card region-item mb-3">
<div class="card-header template-field-head"><strong data-region-title>Region</strong><span class="chip" data-region-chip>Text</span></div>
<div class="card-body p-3 d-grid gap-3">
<div class="region-field-grid region-field-grid--identity">
<label>Region name<input class="form-control" name="region_name[]" value="" placeholder="region_1" required /></label>
</div>
<input type="hidden" name="font_family[]" value="Arial" />
<input type="hidden" name="region_type[]" value="text" />
<input type="hidden" name="region_key[]" value="" />
<input type="hidden" name="region_label[]" value="" />
<div class="region-field-grid region-field-grid--position">
<label>X<input class="form-control" type="number" name="region_x[]" value="80" required /></label>
<label>Y<input class="form-control" type="number" name="region_y[]" value="80" required /></label>
<label>Z-Index<input class="form-control" type="number" name="region_z[]" value="1" required /></label>
</div>
<div class="region-field-grid region-field-grid--size">
<label>Width<input class="form-control" type="number" name="region_width[]" value="300" required /></label>
<label>Height<input class="form-control" type="number" name="region_height[]" value="120" required /></label>
<label>Lock ratio<input class="form-control" type="text" name="region_lock_ratio[]" value="" placeholder="16:9 or 5:4" /></label>
</div>
<div class="d-flex justify-content-end"><button type="button" class="btn btn-sm btn-outline-danger remove-region">Remove</button></div>
</div>
</div>
</template>
<textarea id="template-editor-data" hidden>{{json template.regions}}</textarea>
+166
View File
@@ -0,0 +1,166 @@
<div class="page-header">
<div>
<h2>Edit template - {{template.name}}</h2>
<p>Adjust the canvas and region layout for this template.</p>
</div>
</div>
<form method="post" action="/templates/{{template.id}}" enctype="multipart/form-data" id="template-form" data-async-save data-async-save-close-url="/templates" data-async-save-new-url="/templates/new">
<input type="hidden" name="canvas_size_id" id="canvas-size-id" value="{{template.canvas_size_id}}" />
<input type="hidden" name="canvas_width" id="canvas-width" value="{{template.canvas_size_width}}" />
<input type="hidden" name="canvas_height" id="canvas-height" value="{{template.canvas_size_height}}" />
<input type="hidden" id="canvas-size-summary" />
<input type="hidden" name="existing_background_image_path" value="{{template.background_image_path}}" />
<input type="hidden" name="regions_json" id="regions-json" value="" />
<div class="template-designer-layout">
<div class="card card-outline card-secondary admin-form-card mb-3">
<div class="card-header">
<h3 class="card-title">Designer</h3>
</div>
<div class="card-body">
<div class="template-designer-main">
<div class="designer-stage" id="designer-stage" style="aspect-ratio: {{template.canvas_size_width}} / {{template.canvas_size_height}};">
<img id="background-preview" src="{{template.background_image_path}}" alt="" style="{{#if template.background_image_path}}{{else}}display:none;{{/if}}" />
<div class="empty-bg" id="background-empty" style="{{#if template.background_image_path}}display:none;{{/if}}">No background image selected</div>
<div class="designer-overlay" id="designer-overlay"></div>
</div>
</div>
</div>
</div>
<div class="template-designer-sidebar">
<div class="card card-outline card-primary admin-form-card">
<div class="card-body d-grid gap-2">
<div class="btn-group template-actions-group" role="group" aria-label="Template actions">
{{{saveActionButtons formId="template-form" saveLabel="Save" saveButtonClass="btn btn-success template-save-button" saveAndCloseLabel="Save and Close" saveAndNewLabel="Save and New" saveAndCloseValue="close" saveAndNewValue="new"}}}
<a class="btn btn-warning" href="/templates" data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</a>
<button type="submit" class="btn btn-danger" form="delete-template-form">Delete</button>
</div>
</div>
</div>
<div class="card card-outline card-secondary admin-form-card region-info-card mb-3">
<div class="card-header">
<div>
<h3 class="card-title">Region information</h3>
</div>
</div>
<div class="card-body">
<button type="button" class="btn btn-outline-secondary w-100" id="add-region-button" data-bs-toggle="modal" data-bs-target="#region-add-modal">Add region</button>
<div class="region-selector-block mt-4">
<label for="region-select" class="form-label">Select region</label>
<select id="region-select" class="form-select region-select"></select>
</div>
<div id="region-list" class="region-list mt-3"></div>
</div>
</div>
</div>
</div>
<div class="row g-4 mb-4 align-items-stretch">
<div class="col-12 col-xl-4">
<div class="card card-outline card-primary admin-form-card h-100 template-details-card">
<div class="card-header">
<h3 class="card-title">Template details</h3>
</div>
<div class="card-body">
<div class="row g-3 template-meta-row">
<div class="col-12">
<label for="template-name" class="form-label">Name</label>
<input name="name" id="template-name" class="form-control" value="{{template.name}}" required />
</div>
<div class="col-12">
<label for="canvas-size-select" class="form-label">Canvas size</label>
<select id="canvas-size-select" class="form-select" data-lock-on-existing-template="true">
{{#unless template.canvas_size_id}}
<option value="" data-width="{{template.canvas_size_width}}" data-height="{{template.canvas_size_height}}" selected>{{template.canvas_size_label}}</option>
{{/unless}}
{{#if canvasSizes.length}}
{{#each canvasSizes}}
<option value="{{id}}" data-width="{{width}}" data-height="{{height}}" {{#if (eq ../template.canvas_size_id id)}}selected{{/if}}>{{name}} ({{width}}x{{height}})</option>
{{/each}}
{{/if}}
</select>
</div>
</div>
</div>
</div>
</div>
<div class="col-12 col-xl-8">
<div class="card card-outline card-primary admin-form-card h-100 template-options-card">
<div class="card-header">
<h3 class="card-title">Options</h3>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12 col-md-6">
<label for="background-image" class="form-label">Background image</label>
<div class="template-background-row d-flex gap-2">
<input type="file" name="background_image" id="background-image" class="form-control" accept="image/*" />
<button type="button" class="btn btn-outline-secondary" id="remove-background-image">Remove</button>
</div>
<input type="checkbox" name="remove_background_image" id="remove-background-image-flag" value="1" hidden />
</div>
<div class="col-12 col-md-6">
<label for="background-color" class="form-label">Background colour</label>
<input type="color" name="background_color" id="background-color" class="form-control form-control-color w-100" value="{{#if template.background_color}}{{template.background_color}}{{else}}#111111{{/if}}" title="Choose a background colour" />
</div>
</div>
</div>
</div>
</div>
</div>
</form>
<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>
</div>
</div>
</div>
<template id="region-card-template">
<div class="card card-outline card-secondary admin-form-card region-item mb-3">
<div class="card-header template-field-head"><strong data-region-title>Region</strong><span class="chip" data-region-chip>Text</span></div>
<div class="card-body p-3 d-grid gap-3">
<div class="region-field-grid region-field-grid--identity">
<label>Region name<input class="form-control" name="region_name[]" value="" placeholder="region_1" required /></label>
</div>
<input type="hidden" name="font_family[]" value="Arial" />
<input type="hidden" name="region_type[]" value="text" />
<input type="hidden" name="region_key[]" value="" />
<input type="hidden" name="region_label[]" value="" />
<div class="region-field-grid region-field-grid--position">
<label>X<input class="form-control" type="number" name="region_x[]" value="80" required /></label>
<label>Y<input class="form-control" type="number" name="region_y[]" value="80" required /></label>
<label>Z-Index<input class="form-control" type="number" name="region_z[]" value="1" required /></label>
</div>
<div class="region-field-grid region-field-grid--size">
<label>Width<input class="form-control" type="number" name="region_width[]" value="300" required /></label>
<label>Height<input class="form-control" type="number" name="region_height[]" value="120" required /></label>
<label>Lock ratio<input class="form-control" type="text" name="region_lock_ratio[]" value="" placeholder="16:9 or 5:4" /></label>
</div>
<div class="d-flex justify-content-end"><button type="button" class="btn btn-sm btn-outline-danger remove-region">Remove</button></div>
</div>
</div>
</template>
<textarea id="template-editor-data" hidden>{{json template.regions}}</textarea>
+59
View File
@@ -0,0 +1,59 @@
<div class="page-header">
<div>
<h2>Slide templates</h2>
<p>Design reusable canvas layouts and region definitions.</p>
</div>
</div>
<div class="card card-outline card-primary">
<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>
{{/if}}
</div>
<div class="card-body table-responsive p-0">
<table class="table table-striped w-100 mb-0">
<thead>
<tr>
<th>Name</th>
<th>Canvas</th>
<th>Regions</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{{#if templates.length}}
{{#each templates}}
<tr>
<td data-label="Name">{{name}}</td>
<td data-label="Canvas">{{canvas_size_width}}x{{canvas_size_height}}</td>
<td data-label="Regions">{{regionCount}}</td>
<td data-label="Actions">
{{#if (anyPermission ../currentUser 'templates.update' 'templates.delete')}}
<div class="actions">
{{#if (hasPermission ../currentUser 'templates.update')}}
<a class="btn btn-sm btn-primary" href="/templates/{{id}}/edit">Edit</a>
{{/if}}
{{#if (hasPermission ../currentUser 'templates.delete')}}
<form class="inline-form" method="post" action="/templates/{{id}}/delete" data-confirm-message="Delete this template?">
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
</form>
{{/if}}
</div>
{{else}}
<span class="empty">-</span>
{{/if}}
</td>
</tr>
{{/each}}
{{else}}
<tr><td colspan="4" class="empty">No templates yet.</td></tr>
{{/if}}
</tbody>
</table>
</div>
</div>