Release 2.16.0
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile, web, pulse-signage-web) (push) Successful in 1m46s
Publish Docker Image / build-and-push-existing-registry (./build/Dockerfile.player, player, pulse-signage-player) (push) Successful in 33s

This commit is contained in:
lzstealth committed 2026-10-20 18:52:36 +01:00
1 parent 821593dd8f
commit 3514d514a3
36 files changed
+2329 -346

No files matched your search

+10 -22
View File
@@ -1,23 +1,11 @@
<div class="card border-0 shadow-sm mx-auto" style="max-width: 48rem;">
<div class="card-body p-4 p-md-5">
<div class="d-flex align-items-start gap-3">
<div class="fs-1 text-danger">
<i class="bi bi-exclamation-triangle-fill"></i>
</div>
<div>
<p class="text-uppercase text-muted fw-semibold mb-1">{{statusCode}}</p>
<h1 class="h3 mb-3">{{errorTitle}}</h1>
<p class="mb-4">{{errorMessage}}</p>
{{#if detail}}
<pre class="small bg-body-tertiary border rounded p-3 mb-4 text-wrap">{{detail}}</pre>
{{/if}}
<div class="d-flex gap-2 flex-wrap">
<a class="btn btn-primary" href="{{backUrl}}">{{backLabel}}</a>
{{#if searchUrl}}
<a class="btn btn-outline-secondary" href="{{searchUrl}}">Search</a>
{{/if}}
</div>
</div>
</div>
<section class="error-page-content text-center">
<p class="display-1 fw-bold text-{{statusVariant}} mb-3" aria-label="Error {{statusCode}}">{{statusCode}}</p>
<h1 class="h2 mb-3">{{errorTitle}}</h1>
<p class="text-body-secondary mb-4">{{errorMessage}}</p>
<div class="d-flex justify-content-center gap-2 flex-wrap">
{{#if searchUrl}}
<a class="btn btn-primary" href="{{searchUrl}}"><i class="bi bi-search me-2" aria-hidden="true"></i>Search</a>
{{/if}}
<a class="btn btn-outline-secondary" href="{{backUrl}}"><i class="bi bi-arrow-left me-2" aria-hidden="true"></i>{{backLabel}}</a>
</div>
</div>
</section>
+87 -1
View File
@@ -8,6 +8,7 @@
<div class="slide-editor-shell">
<link rel="stylesheet" href="/assets/vendor/cropperjs/cropper.min.css" />
<form method="post" action="{{action}}" enctype="multipart/form-data" id="slide-form" class="slide-form-stack" {{{formAttrs}}}>
<input type="hidden" id="slide-layouts-json" name="region_layouts_json" value="{}" />
<div class="card card-outline card-primary admin-form-card slide-template-card">
<div class="card-header bg-body-tertiary">
<h3 class="card-title">Slide details</h3>
@@ -68,12 +69,92 @@
<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 class="d-flex justify-content-end gap-2 mt-3">
<button type="button" class="btn btn-outline-secondary flex-grow-1" id="open-preview-popup"><i class="bi bi-box-arrow-up-right me-1" aria-hidden="true"></i>Open popup preview</button>
<button type="button" class="btn btn-outline-primary flex-shrink-0" id="adjust-slide-layout" title="Adjust layout" aria-label="Adjust layout" disabled><i class="bi bi-bounding-box" aria-hidden="true"></i></button>
<button type="button" class="btn btn-outline-secondary flex-shrink-0" id="adjust-slide-animations" title="Adjust animations" aria-label="Adjust animations" disabled><i class="bi bi-stars" aria-hidden="true"></i></button>
</div>
</div>
</aside>
</div>
</div>
{{#> modal-shell modalId="slide-layout-modal" modalLabelId="slide-layout-modal-label" modalDialogClass="modal-xl modal-dialog-centered modal-dialog-scrollable slide-layout-dialog"}}
<div class="modal-header">
<h2 class="modal-title fs-5" id="slide-layout-modal-label">Adjust layout <span class="text-body-secondary fs-6">This slide only</span></h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cancel layout changes"></button>
</div>
<div class="modal-body slide-layout-body">
<div class="slide-layout-workspace" id="slide-layout-workspace"></div>
<aside class="slide-layout-properties">
<label for="slide-layout-region" class="form-label">Region</label>
<select id="slide-layout-region" class="form-select mb-3"></select>
<div id="slide-layout-fields" class="d-grid gap-3">{{> signage/templates/region-layout-fields slideLayout=true}}</div>
<div class="form-check form-switch my-3"><input type="checkbox" id="slide-layout-snap" class="form-check-input" checked /><label for="slide-layout-snap" class="form-check-label">Snap to guides</label></div>
<div class="btn-group mb-3" role="group" aria-label="Layout history">
<button type="button" id="slide-layout-undo" class="btn btn-outline-secondary" title="Undo" aria-label="Undo"><i class="bi bi-arrow-counterclockwise" aria-hidden="true"></i></button>
<button type="button" id="slide-layout-redo" class="btn btn-outline-secondary" title="Redo" aria-label="Redo"><i class="bi bi-arrow-clockwise" aria-hidden="true"></i></button>
</div>
<button type="button" id="slide-layout-reset" class="btn btn-outline-secondary w-100 mb-2"><i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset region to template</button>
<button type="button" id="slide-layout-reset-all" class="btn btn-outline-secondary w-100"><i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset all to template</button>
</aside>
</div>
<div class="modal-footer">
<span id="slide-layout-save-status" class="small me-auto" role="status" aria-live="polite"></span>
<div class="btn-group dropup" role="group" aria-label="Layout save actions">
<button type="button" class="btn btn-primary" id="slide-layout-apply">Apply to this slide</button>
{{#if (hasPermission currentUser 'templates.update')}}
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false" title="Layout save options" aria-label="Layout save options"></button>
<div class="dropdown-menu dropdown-menu-end">
<button type="button" class="dropdown-item" id="save-layout-to-template">Save layout to shared template</button>
</div>
{{/if}}
</div>
<button type="button" class="btn btn-warning" data-bs-dismiss="modal">Cancel</button>
</div>
{{/modal-shell}}
{{#> modal-shell modalId="slide-animation-modal" modalLabelId="slide-animation-modal-label" modalDialogClass="modal-xl modal-dialog-centered modal-dialog-scrollable slide-layout-dialog"}}
<div class="modal-header">
<h2 class="modal-title fs-5" id="slide-animation-modal-label">Adjust animations <span class="text-body-secondary fs-6">This slide only</span></h2>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cancel animation changes"></button>
</div>
<div class="modal-body slide-animation-body">
<section class="slide-animation-preview-panel">
<div id="slide-animation-preview"></div>
</section>
<div class="slide-animation-controls">
<div class="btn-group w-100 mb-3" role="group" aria-label="Animation preview">
<button type="button" class="btn btn-outline-secondary" data-slide-animation-preview="intro" aria-pressed="false"><i class="bi bi-play-fill me-1" aria-hidden="true" data-animation-preview-icon></i>Intro</button>
<button type="button" class="btn btn-outline-secondary" data-slide-animation-preview="outro" aria-pressed="false"><i class="bi bi-play-fill me-1" aria-hidden="true" data-animation-preview-icon></i>Outro</button>
<button type="button" class="btn btn-outline-secondary" data-slide-animation-preview="loop" aria-pressed="false"><i class="bi bi-play-fill me-1" aria-hidden="true" data-animation-preview-icon></i>Attention</button>
</div>
<label for="slide-animation-region" class="form-label">Region</label>
<select id="slide-animation-region" class="form-select mb-3"></select>
<div id="slide-animation-fields" class="d-grid gap-3">
{{> signage/templates/region-animation-controls}}
<div id="slide-animation-advanced" class="d-grid gap-3" hidden>
<h3 class="h6 mb-0">Advanced animation settings</h3>
<div class="slide-animation-advanced-fields" data-animation-advanced-fields>{{> signage/templates/animation-advanced-fields}}</div>
</div>
</div>
<button type="button" id="slide-animation-reset-region" class="btn btn-outline-secondary w-100 mt-3"><i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset region animations to template</button>
<button type="button" id="slide-animation-reset-all" class="btn btn-outline-secondary w-100 mt-2"><i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Reset all animations to template</button>
</div>
</div>
<div class="modal-footer">
<span id="slide-animation-save-status" class="small me-auto" role="status" aria-live="polite"></span>
<div class="btn-group dropup" role="group" aria-label="Animation save actions">
<button type="button" class="btn btn-primary" id="slide-animation-apply">Apply to this slide</button>
{{#if (hasPermission currentUser 'templates.update')}}
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false" title="Animation save options" aria-label="Animation save options"></button>
<div class="dropdown-menu dropdown-menu-end"><button type="button" class="dropdown-item" id="save-animations-to-template">Save animations to shared template</button></div>
{{/if}}
</div>
<button type="button" class="btn btn-warning" data-bs-dismiss="modal">Cancel</button>
</div>
{{/modal-shell}}
{{#> modal-shell modalId="slide-image-cropper-modal" modalLabelId="slide-image-cropper-modal-label" modalDialogClass="modal-dialog-centered modal-xl modal-dialog-scrollable" modalBackdropStatic=true modalKeyboardDisabled=true}}
<div class="modal-header">
<div>
@@ -125,6 +206,11 @@
{{/if}}
<textarea id="slide-editor-data" hidden>{{json slideEditorData}}</textarea>
<script src="/assets/js/lib/modal.js?v={{appVersion}}"></script>
<script src="/assets/js/shared/region-layout.js?v={{appVersion}}"></script>
<script src="/assets/js/templates/animation-presets.js?v={{appVersion}}"></script>
<script src="/assets/js/templates/template-designer-utils.js?v={{appVersion}}"></script>
<script src="/assets/js/shared/region-animation-controls.js?v={{appVersion}}"></script>
<script src="/assets/js/shared/qr-code-svg.js?v={{appVersion}}"></script>
<script src="/assets/js/shared/upload-utils.js?v={{appVersion}}"></script>
<script src="/assets/vendor/cropperjs/cropper.min.js"></script>
@@ -0,0 +1,33 @@
<div class="row g-3">
<div class="col-12 col-lg-4">
<div class="card border-secondary-subtle">
<div class="card-header bg-transparent"><strong>Intro</strong></div>
<div class="card-body d-grid gap-3 align-content-start">
<label class="d-grid gap-1">Preset<select class="form-select" data-animation-modal-step="intro" data-animation-modal-field="preset">{{#each animationPresets.advanced.intro}}<option value="{{value}}">{{label}}</option>{{/each}}</select></label>
<label class="d-grid gap-1">Duration %<input class="form-control" type="number" min="1" max="200" step="1" data-animation-modal-step="intro" data-animation-modal-field="duration_ms" placeholder="100" /></label>
<label class="d-grid gap-1">Delay ms<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="intro" data-animation-modal-field="delay_ms" placeholder="0" /></label>
</div>
</div>
</div>
<div class="col-12 col-lg-4">
<div class="card border-secondary-subtle">
<div class="card-header bg-transparent"><strong>Outro</strong></div>
<div class="card-body d-grid gap-3 align-content-start">
<label class="d-grid gap-1">Preset<select class="form-select" data-animation-modal-step="outro" data-animation-modal-field="preset">{{#each animationPresets.advanced.outro}}<option value="{{value}}">{{label}}</option>{{/each}}</select></label>
<label class="d-grid gap-1">Duration %<input class="form-control" type="number" min="1" max="200" step="1" data-animation-modal-step="outro" data-animation-modal-field="duration_ms" placeholder="100" /></label>
<label class="d-grid gap-1">Delay ms<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="outro" data-animation-modal-field="delay_ms" placeholder="0" /></label>
</div>
</div>
</div>
<div class="col-12 col-lg-4">
<div class="card border-secondary-subtle">
<div class="card-header bg-transparent"><strong>Attention seekers</strong></div>
<div class="card-body d-grid gap-3 align-content-start">
<label class="d-grid gap-1">Preset<select class="form-select" data-animation-modal-step="loop" data-animation-modal-field="preset">{{#each animationPresets.advanced.attentionSeekers}}<option value="{{value}}">{{label}}</option>{{/each}}</select></label>
<label class="d-grid gap-1">Duration %<input class="form-control" type="number" min="1" max="200" step="1" data-animation-modal-step="loop" data-animation-modal-field="duration_ms" placeholder="100" /></label>
<label class="d-grid gap-1">Delay ms<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="loop" data-animation-modal-field="delay_ms" placeholder="0" /></label>
<label class="d-grid gap-1">Repeat<input class="form-control" type="number" min="1" max="999" step="1" data-animation-modal-step="loop" data-animation-modal-field="iterations" placeholder="1" /></label>
</div>
</div>
</div>
</div>
@@ -6,89 +6,7 @@
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body d-grid gap-4">
<div class="row g-3">
<div class="col-12 col-lg-4">
<div class="card border-secondary-subtle">
<div class="card-header bg-transparent">
<strong class="text-capitalize">Intro</strong>
</div>
<div class="card-body d-grid gap-3 align-content-start">
<label class="d-grid gap-1">
Preset
<select class="form-select" data-animation-modal-step="intro" data-animation-modal-field="preset">
{{#each animationPresets.advanced.intro}}
<option value="{{value}}">{{label}}</option>
{{/each}}
</select>
</label>
<label class="d-grid gap-1">
Duration %
<input class="form-control" type="number" min="1" max="200" step="1" data-animation-modal-step="intro" data-animation-modal-field="duration_ms" placeholder="100" />
</label>
<label class="d-grid gap-1">
Delay ms
<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="intro" data-animation-modal-field="delay_ms" placeholder="0" />
</label>
</div>
</div>
</div>
<div class="col-12 col-lg-4">
<div class="card border-secondary-subtle">
<div class="card-header bg-transparent">
<strong class="text-capitalize">Outro</strong>
</div>
<div class="card-body d-grid gap-3 align-content-start">
<label class="d-grid gap-1">
Preset
<select class="form-select" data-animation-modal-step="outro" data-animation-modal-field="preset">
{{#each animationPresets.advanced.outro}}
<option value="{{value}}">{{label}}</option>
{{/each}}
</select>
</label>
<label class="d-grid gap-1">
Duration %
<input class="form-control" type="number" min="1" max="200" step="1" data-animation-modal-step="outro" data-animation-modal-field="duration_ms" placeholder="100" />
</label>
<label class="d-grid gap-1">
Delay ms
<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="outro" data-animation-modal-field="delay_ms" placeholder="0" />
</label>
</div>
</div>
</div>
<div class="col-12 col-lg-4">
<div class="card border-secondary-subtle">
<div class="card-header bg-transparent">
<strong class="text-capitalize">Attention seekers</strong>
</div>
<div class="card-body d-grid gap-3 align-content-start">
<label class="d-grid gap-1">
Preset
<select class="form-select" data-animation-modal-step="loop" data-animation-modal-field="preset">
{{#each animationPresets.advanced.attentionSeekers}}
<option value="{{value}}">{{label}}</option>
{{/each}}
</select>
</label>
<label class="d-grid gap-1">
Duration %
<input class="form-control" type="number" min="1" max="200" step="1" data-animation-modal-step="loop" data-animation-modal-field="duration_ms" placeholder="100" />
</label>
<label class="d-grid gap-1">
Delay ms
<input class="form-control" type="number" min="0" step="1" data-animation-modal-step="loop" data-animation-modal-field="delay_ms" placeholder="0" />
</label>
<label class="d-grid gap-1">
Repeat
<input class="form-control" type="number" min="1" max="999" step="1" data-animation-modal-step="loop" data-animation-modal-field="iterations" placeholder="1" />
</label>
</div>
</div>
</div>
</div>
</div>
<div class="modal-body d-grid gap-4">{{> signage/templates/animation-advanced-fields}}</div>
<div class="modal-footer justify-content-between">
<button type="button" class="btn btn-outline-secondary" data-animation-modal-reset>Reset to defaults</button>
<div class="d-flex gap-2">
+4 -57
View File
@@ -16,15 +16,16 @@
<div class="template-designer-layout">
<div class="card card-outline card-secondary admin-form-card mb-3">
<div class="card-header bg-body-tertiary">
<div class="card-header bg-body-tertiary d-flex align-items-center justify-content-between template-designer-main-header">
<h3 class="card-title">Designer</h3>
<div class="form-check form-switch template-snap-control mb-0 ms-auto"><label for="template-layout-snap" class="form-check-label">Snap to guides</label><input type="checkbox" id="template-layout-snap" class="form-check-input" checked /></div>
</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="opacity:0.4;{{#if template.background_image_path}}display:none;{{/if}}">No background image selected</div>
<div class="designer-overlay" id="designer-overlay"></div>
<div class="designer-overlay" id="designer-overlay" tabindex="0" role="group" aria-label="Template layout"></div>
</div>
</div>
</div>
@@ -168,61 +169,7 @@
<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[]" min="1" step="1" 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[]" min="1" step="1" value="300" required /></label>
<label>Height<input class="form-control" type="number" name="region_height[]" min="1" step="1" value="120" required /></label>
<label>Lock ratio<input class="form-control" type="text" name="region_lock_ratio[]" value="" maxlength="20" data-limit-text-length placeholder="Ratio, e.g. 16:9" /></label>
<label>Remove<button type="button" class="btn btn-outline-danger w-100" data-region-remove-button{{#if isEdit}} disabled{{/if}}>Remove</button></label>
</div>
<div class="region-field-grid region-field-grid--animation">
<div class="d-grid gap-1">
<label>
Intro
<select class="form-select" data-animation-preset="intro">
{{#each animationPresets.basic.intro}}
<option value="{{value}}">{{label}}</option>
{{/each}}
</select>
</label>
<button type="button" class="btn btn-outline-secondary w-100" data-animation-reset-button="intro" hidden>Reset to basic</button>
</div>
<div class="d-grid gap-1">
<label>
Outro
<select class="form-select" data-animation-preset="outro">
{{#each animationPresets.basic.outro}}
<option value="{{value}}">{{label}}</option>
{{/each}}
</select>
</label>
<button type="button" class="btn btn-outline-secondary w-100" data-animation-reset-button="outro" hidden>Reset to basic</button>
</div>
</div>
<div class="region-field-grid region-field-grid--animation">
<div class="d-grid gap-1">
<label>
Attention seekers
<select class="form-select" data-animation-preset="loop">
{{#each animationPresets.basic.attentionSeekers}}
<option value="{{value}}">{{label}}</option>
{{/each}}
</select>
</label>
<button type="button" class="btn btn-outline-secondary w-100" data-animation-reset-button="loop" hidden>Reset to basic</button>
</div>
<div class="d-grid gap-1">
<label>
Advanced
<button type="button" class="btn btn-outline-secondary w-100" data-animation-advanced-button>Advanced</button>
</label>
</div>
</div>
<textarea class="visually-hidden" name="region_animation_json[]" data-animation-json-input rows="1"></textarea>
{{> signage/templates/region-layout-fields}}
</div>
</div>
</div>
@@ -0,0 +1,18 @@
<div class="region-field-grid region-field-grid--animation">
<div class="d-grid gap-1">
<label>Intro<select class="form-select" data-animation-preset="intro">{{#each animationPresets.basic.intro}}<option value="{{value}}">{{label}}</option>{{/each}}</select></label>
<button type="button" class="btn btn-outline-secondary w-100" data-animation-reset-button="intro" hidden>Reset to basic</button>
</div>
<div class="d-grid gap-1">
<label>Outro<select class="form-select" data-animation-preset="outro">{{#each animationPresets.basic.outro}}<option value="{{value}}">{{label}}</option>{{/each}}</select></label>
<button type="button" class="btn btn-outline-secondary w-100" data-animation-reset-button="outro" hidden>Reset to basic</button>
</div>
</div>
<div class="region-field-grid region-field-grid--animation">
<div class="d-grid gap-1">
<label>Attention seekers<select class="form-select" data-animation-preset="loop">{{#each animationPresets.basic.attentionSeekers}}<option value="{{value}}">{{label}}</option>{{/each}}</select></label>
<button type="button" class="btn btn-outline-secondary w-100" data-animation-reset-button="loop" hidden>Reset to basic</button>
</div>
<div class="d-grid gap-1"><label>Advanced<button type="button" class="btn btn-outline-secondary w-100" data-animation-advanced-button aria-expanded="false">Advanced</button></label></div>
</div>
<textarea class="visually-hidden" name="region_animation_json[]" data-animation-json-input rows="1"></textarea>
@@ -0,0 +1,14 @@
<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[]" min="1" step="1" 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[]" min="1" step="1" value="300" required /></label>
<label>Height<input class="form-control" type="number" name="region_height[]" min="1" step="1" value="120" required /></label>
<label>Lock ratio<input class="form-control" type="text" name="region_lock_ratio[]" value="" maxlength="20" data-limit-text-length placeholder="Ratio, e.g. 16:9" /></label>
{{#unless slideLayout}}<label>Remove<button type="button" class="btn btn-outline-danger w-100" data-region-remove-button{{#if isEdit}} disabled{{/if}}>Remove</button></label>{{/unless}}
</div>
{{#unless slideLayout}}
{{> signage/templates/region-animation-controls}}
{{/unless}}