Release v2.4.2

This commit is contained in:
2026-08-03 12:45:27 +01:00
parent 2b9cabdab2
commit e95928f31c
141 changed files with 7694 additions and 5078 deletions
-28
View File
@@ -1,28 +0,0 @@
<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>
<button type="submit" class="btn btn-outline-danger" disabled>Delete</button>
</div>
</div>
</form>
</div>
-194
View File
@@ -1,194 +0,0 @@
<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-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>
<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>
{{#if inUse}}
<button type="submit" class="btn btn-outline-danger" form="delete-playlist-form" disabled>Delete</button>
{{else}}
<button type="submit" class="btn btn-danger" form="delete-playlist-form">Delete</button>
{{/if}}
</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 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>
</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}}" data-media-type="{{media_type}}" data-media-path="{{media_path}}" data-video-source-path="{{videoSourcePath}}" data-video-duration-seconds="{{videoDurationSeconds}}" data-use-video-duration-state="{{#if useVideoDuration}}true{{else}}false{{/if}}">
<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">
<div class="playlist-slide-cell">
<div class="playlist-slide-thumb" aria-hidden="true" {{#if canvas_width}}style="--playlist-slide-thumb-aspect-ratio: {{canvas_width}} / {{canvas_height}};"{{/if}}>
{{#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">
<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">
<div class="playlist-duration-field">
<input name="duration_seconds[]" type="text" inputmode="decimal" value="{{durationSeconds}}" required form="playlist-edit-form" class="form-control form-control-sm playlist-duration-input text-end" {{#if useVideoDuration}}disabled{{/if}} />
<input type="hidden" name="use_video_duration[]" value="{{#if useVideoDuration}}1{{else}}0{{/if}}" form="playlist-edit-form" />
{{#if useVideoDuration}}
<input type="hidden" name="duration_seconds[]" value="{{durationSeconds}}" form="playlist-edit-form" data-video-duration-mirror />
{{/if}}
{{#if showVideoDurationButton}}
<button type="button" class="btn btn-outline-secondary btn-sm playlist-use-video-duration{{#if useVideoDuration}} active{{/if}}" data-use-video-duration-button aria-pressed="{{#if useVideoDuration}}true{{else}}false{{/if}}">{{#if useVideoDuration}}Use Video Duration{{else}}Use video duration{{/if}}</button>
{{/if}}
</div>
</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>
<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 d-flex flex-column flex-lg-row gap-3 align-items-stretch align-items-lg-end">
<div class="flex-grow-1">
<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-filter mb-0">
<button type="button" class="btn btn-outline-secondary playlist-slide-picker-filter-button" id="playlist-slide-picker-show-assigned" aria-pressed="false">Show already added slides</button>
</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>
+121
View File
@@ -0,0 +1,121 @@
<div class="page-header">
<div>
<h2>{{#if isEdit}}Edit playlist - {{playlist.name}}{{else}}Add playlist{{/if}}</h2>
<p>Set playlist details, items, and schedules.</p>
</div>
</div>
<div class="row g-4 mb-4 align-items-stretch">
<div class="col-12 col-lg-6">
<div class="card card-outline card-primary admin-form-card h-100">
<div class="card-header">
<h3 class="card-title">Playlist details</h3>
</div>
<form id="playlist-edit-form" method="post" action="{{formAction}}" {{{formAttrs}}}>
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-12 col-lg-7">
<label for="playlist-name" class="form-label">Name</label>
<input id="playlist-name" name="name" class="form-control" value="{{playlist.name}}" placeholder="Lobby loop" required />
</div>
<div class="col-12 col-lg-5">
<label for="playlist-canvas-size" class="form-label">Canvas size</label>
<select id="playlist-canvas-size" name="canvas_size_id" class="form-select" form="playlist-edit-form" data-playlist-canvas-editable="{{#if playlistCanvasEditable}}true{{else}}false{{/if}}" {{#unless playlistCanvasEditable}}disabled{{/unless}} {{#unless playlistCanvasId}}required{{/unless}}>
<option value="">Select a canvas size</option>
{{#each canvasSizes}}
<option value="{{id}}" {{#if selected}}selected{{/if}}>{{label}}</option>
{{/each}}
</select>
<input type="hidden" id="playlist-canvas-size-lock" name="canvas_size_id" value="{{playlistCanvasSizeId}}" form="playlist-edit-form" {{#if playlistCanvasEditable}}disabled{{/if}} />
</div>
</div>
</div>
<div class="card-footer d-flex justify-content-end">
{{{saveActionButtons formId="playlist-edit-form" saveUrl=formAction cancelUrl=cancelUrl deleteUrl=deleteUrl deleteConfirmMessage="Delete this playlist?" deleteDisabled=deleteDisabled showSaveAndClose=showSaveSecondaryActions showSaveAndNew=showSaveSecondaryActions}}}
</div>
</form>
</div>
</div>
<div class="col-12 col-lg-6">
<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 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 unavailable</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 class="card-tools d-flex flex-nowrap align-items-center gap-2">
<button type="button" class="btn btn-primary btn-sm" id="playlist-open-slide-modal" data-bs-toggle="modal" data-bs-target="#playlist-add-slide-modal" {{#unless playlistCanvasId}}disabled{{/unless}}>Add slide</button>
</div>
</div>
<div class="card-body p-0 playlist-items-table-responsive">
<table class="table table-striped w-100 mb-0">
<thead><tr><th>Order</th><th>Slide</th><th>Schedule</th><th>Config</th><th>Actions</th></tr></thead>
<tbody id="playlist-items-body" data-playlist-id="{{playlist.id}}" data-playlist-canvas-id="{{playlistCanvasId}}">
{{#if playlistSlides.length}}
{{#each playlistSlides}}
{{> signage/playlists/slide-row slide=this playlistScheduleConfigUrl=../playlistScheduleConfigUrl}}
{{/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>
<template id="playlist-slide-row-template">
{{> signage/playlists/slide-row slide=playlistSlideTemplate playlistScheduleConfigUrl=playlistScheduleConfigUrl}}
</template>
<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 d-flex flex-column flex-lg-row gap-3 align-items-stretch align-items-lg-end">
<div class="flex-grow-1">
<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-filter mb-0">
<button type="button" class="btn btn-outline-secondary playlist-slide-picker-filter-button" id="playlist-slide-picker-show-assigned" aria-pressed="false">Show already added slides</button>
</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}}
@@ -0,0 +1,46 @@
<div class="card card-widget card-outline card-secondary mb-3 schedule-rule-card{{#if isCollapsed}} collapsed-card{{/if}}" data-schedule-rule-card>
<div class="card-header py-2 d-flex align-items-center">
<div class="flex-grow-1 min-w-0">
<h4 class="h6 mb-0">Rule <span data-schedule-rule-number>{{ruleIndex}}</span></h4>
<div class="text-body-secondary small" data-schedule-rule-summary>{{ruleSummary}}</div>
</div>
<div class="d-flex align-items-center gap-4 ms-auto flex-shrink-0">
<button type="button" class="btn btn-sm btn-outline-danger" data-remove-schedule-rule data-confirm-message="Remove this rule?">Remove</button>
<button type="button" class="btn btn-tool p-0 text-body-secondary" data-lte-toggle="card-collapse" aria-label="Collapse rule {{ruleIndex}}" title="Collapse rule {{ruleIndex}}">
<i class="bi bi-dash-lg" aria-hidden="true" data-lte-icon="collapse"></i>
<i class="bi bi-plus-lg" aria-hidden="true" data-lte-icon="expand"></i>
<span class="visually-hidden">Collapse rule {{ruleIndex}}</span>
</button>
</div>
</div>
<div class="card-body">
<input type="hidden" name="schedule_rule_row_key[]" value="" data-schedule-rule-row-key />
<input type="hidden" name="schedule_rule_position[]" value="0" data-schedule-rule-position />
<div class="row g-3">
<div class="col-12 col-md-6">
<label class="form-label">Start datetime</label>
<input type="datetime-local" class="form-control" name="schedule_rule_start_datetime[]" value="{{startDateTimeValue}}" data-schedule-rule-start-datetime />
</div>
<div class="col-12 col-md-6">
<label class="form-label">End datetime</label>
<input type="datetime-local" class="form-control" name="schedule_rule_end_datetime[]" value="{{endDateTimeValue}}" data-schedule-rule-end-datetime />
</div>
<div class="col-12 col-md-6">
<label class="form-label">Start time</label>
<input type="time" class="form-control" name="schedule_rule_start_time[]" value="{{startTimeValue}}" data-schedule-rule-start-time />
</div>
<div class="col-12 col-md-6">
<label class="form-label">End time</label>
<input type="time" class="form-control" name="schedule_rule_end_time[]" value="{{endTimeValue}}" data-schedule-rule-end-time />
</div>
<div class="col-12">
<div class="btn-group btn-group-sm w-100 schedule-day-grid" role="group" aria-label="Schedule days">
{{#each dayOptions}}
<input class="btn-check" type="checkbox" id="{{inputId}}" value="{{value}}" autocomplete="off" {{#if checked}}checked{{/if}} data-schedule-rule-day />
<label class="btn btn-outline-primary flex-fill schedule-day-button" for="{{inputId}}">{{label}}</label>
{{/each}}
</div>
</div>
</div>
</div>
</div>
@@ -1,67 +1,35 @@
<div class="card card-outline card-primary mb-0 slide-schedule-card">
<div class="card-header">
<h3 class="card-title">Slide schedule</h3>
<div class="card-header d-flex align-items-center gap-2">
<h3 class="card-title mb-0">Slide schedule</h3>
<div class="card-tools ms-auto">
<button type="button" class="btn btn-primary btn-sm" id="schedule-add-rule">Add another rule</button>
</div>
</div>
<form method="post" action="/playlists/{{playlist.id}}/slides/{{playlistSlide.id}}/config">
<form method="post" action="{{#if playlist.id}}/playlists/{{playlist.id}}/slides/{{playlistSlide.id}}/config{{else}}#{{/if}}" id="playlist-slide-schedule-form" {{#unless playlist.id}}data-schedule-draft="true"{{/unless}}>
<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 class="card card-outline card-info mb-3">
<div class="card-body py-3">
<p class="text-body-secondary mb-0">These rules work separately - the playlist plays whenever any of them match.</p>
</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 id="schedule-rule-list">
{{#each scheduleRules}}
{{> signage/playlists/schedule-rule-card isCollapsed=true}}
{{/each}}
</div>
<template id="schedule-rule-card-template">
{{> signage/playlists/schedule-rule-card ruleIndex=1 ruleSummary="" startDateTimeValue="" endDateTimeValue="" startTimeValue="" endTimeValue="" dayOptions=templateDayOptions isCollapsed=true}}
</template>
</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>
<button type="button" class="btn btn-warning" data-schedule-cancel data-confirm-unsaved="You have unsaved changes. Leave this page?">Cancel</button>
</div>
</div>
</form>
@@ -0,0 +1,54 @@
<tr data-playlist-slide-row data-row-key="{{slide.row_key}}" data-slide-id="{{slide.slide_id}}" data-canvas-signature="{{slide.canvasSignature}}" data-media-type="{{slide.media_type}}" data-media-path="{{slide.media_path}}" data-video-source-path="{{slide.videoSourcePath}}" data-video-duration-seconds="{{slide.videoDurationSeconds}}" data-use-video-duration-state="{{#if slide.useVideoDuration}}true{{else}}false{{/if}}" data-disable-audio-state="{{#if slide.disableAudio}}true{{else}}false{{/if}}">
<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">{{slide.displayOrder}}</span>
</div>
</td>
<td data-label="Slide">
<div class="playlist-slide-cell">
<div class="playlist-slide-thumb" aria-hidden="true" {{#if slide.canvas_width}}style="--playlist-slide-thumb-aspect-ratio: {{slide.canvas_width}} / {{slide.canvas_height}};"{{/if}} data-playlist-slide-thumb>
{{#if slide.thumbnail_path}}
<img class="playlist-slide-thumb-image" src="{{slide.thumbnail_path}}" alt="" loading="lazy" data-playlist-slide-thumb-image />
{{else}}
<span class="playlist-slide-thumb-placeholder" data-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" data-playlist-slide-title>{{slide.title}}</span>
</div>
</div>
<input type="hidden" name="slide_id[]" value="{{slide.slide_id}}" form="playlist-edit-form" data-playlist-slide-id-input />
<input type="hidden" name="row_key[]" value="{{slide.row_key}}" form="playlist-edit-form" data-playlist-row-key-input />
</td>
<td data-label="Rules">
<div class="playlist-schedule-summary"><span class="chip" data-schedule-summary-chip>{{slide.scheduleSummary}}</span></div>
<div class="d-none" data-schedule-rules-container data-schedule-rules='{{json slide.scheduleRules}}'></div>
</td>
<td data-label="Config">
<div class="playlist-duration-field">
<input name="duration_seconds[]" type="text" inputmode="decimal" value="{{slide.durationSeconds}}" required form="playlist-edit-form" class="form-control form-control-sm playlist-duration-input text-end" {{#if slide.useVideoDuration}}disabled{{/if}} data-playlist-duration-input />
<input type="hidden" name="use_video_duration[]" value="{{#if slide.useVideoDuration}}1{{else}}0{{/if}}" form="playlist-edit-form" data-playlist-use-video-duration-input />
<input type="hidden" name="disable_audio[]" value="{{#if slide.disableAudio}}1{{else}}0{{/if}}" form="playlist-edit-form" data-playlist-disable-audio-input />
{{#if slide.useVideoDuration}}
<input type="hidden" name="duration_seconds[]" value="{{slide.durationSeconds}}" form="playlist-edit-form" data-video-duration-mirror data-playlist-duration-mirror />
{{/if}}
<button type="button" class="btn btn-outline-info btn-sm playlist-use-video-duration{{#if slide.useVideoDuration}} active{{/if}}" data-use-video-duration-button aria-pressed="{{#if slide.useVideoDuration}}true{{else}}false{{/if}}" aria-label="{{#if slide.useVideoDuration}}Disable use video duration{{else}}Use video duration{{/if}}" title="{{#if slide.useVideoDuration}}Disable use video duration{{else}}Use video duration{{/if}}" {{#unless slide.showVideoDurationButton}}hidden{{/unless}}><i class="bi {{#if slide.useVideoDuration}}bi-camera-video{{else}}bi-camera-video-off{{/if}}" aria-hidden="true" data-use-video-duration-icon></i><span class="visually-hidden">{{#if slide.useVideoDuration}}Disable use video duration{{else}}Use video duration{{/if}}</span></button>
<button type="button" class="btn {{#if slide.disableAudio}}btn-outline-secondary{{else}}btn-secondary active{{/if}} btn-sm playlist-disable-audio" data-disable-audio-button aria-pressed="{{#if slide.disableAudio}}true{{else}}false{{/if}}" aria-label="{{#if slide.disableAudio}}Enable audio{{else}}Mute slide{{/if}}" title="{{#if slide.disableAudio}}Enable audio{{else}}Mute slide{{/if}}" {{#unless slide.showMuteButton}}hidden{{/unless}}><i class="bi {{#if slide.disableAudio}}bi-volume-mute{{else}}bi-volume-up{{/if}}" aria-hidden="true" data-disable-audio-icon></i><span class="visually-hidden">{{#if slide.disableAudio}}Enable audio{{else}}Mute slide{{/if}}</span></button>
</div>
</td>
<td data-label="Actions">
<div class="actions playlist-item-actions">
<button type="button" class="btn btn-sm btn-primary" data-schedule-config="{{playlistScheduleConfigUrl}}" data-schedule-config-row="{{slide.row_key}}">Schedule</button>
<button type="button" class="btn btn-sm btn-danger" data-playlist-remove-row>Remove</button>
</div>
</td>
</tr>