Release v1.3

This commit is contained in:
2026-01-25 15:57:38 +01:00
parent 47aca9d64d
commit 56760e380d
10 changed files with 370 additions and 5 deletions

View File

@@ -96,6 +96,7 @@
data-display-name="{{ d.name }}"
data-current-desc="{{ d.description or '' }}"
data-current-transition="{{ d.transition or 'none' }}"
data-current-show-overlay="{{ '1' if d.show_overlay else '0' }}"
data-legacy-playlist-id="{{ d.assigned_playlist_id or '' }}"
data-active-playlist-ids="{{ d.display_playlists | map(attribute='playlist_id') | list | join(',') }}"
>
@@ -157,6 +158,12 @@
</select>
<div class="form-text">Applied on the display when switching between playlist items.</div>
</div>
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" id="editPlaylistsShowOverlayCheck" />
<label class="form-check-label" for="editPlaylistsShowOverlayCheck">Show company overlay</label>
<div class="form-text">If your company has an overlay uploaded, it will be displayed on top of the content.</div>
</div>
<hr class="my-3" />
<div class="text-muted small mb-2">Tick the playlists that should be active on this display.</div>
<div id="editPlaylistsList" class="d-flex flex-column gap-2"></div>
@@ -301,6 +308,7 @@
const plDescInputEl = document.getElementById('editPlaylistsDescInput');
const plDescCountEl = document.getElementById('editPlaylistsDescCount');
const plTransitionEl = document.getElementById('editPlaylistsTransitionSelect');
const plShowOverlayEl = document.getElementById('editPlaylistsShowOverlayCheck');
let activePlDisplayId = null;
let activePlButton = null;
@@ -365,6 +373,11 @@
const currentTransition = (btn.dataset.currentTransition || 'none').toLowerCase();
if (plTransitionEl) plTransitionEl.value = ['none','fade','slide'].includes(currentTransition) ? currentTransition : 'none';
if (plShowOverlayEl) {
const raw = (btn.dataset.currentShowOverlay || '').toLowerCase();
plShowOverlayEl.checked = raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
}
const selected = computeActiveIdsFromDataset(btn);
renderPlaylistCheckboxes(selected);
if (plHintEl) {
@@ -379,11 +392,12 @@
const ids = getSelectedPlaylistIdsFromModal();
const desc = plDescInputEl ? (plDescInputEl.value || '').trim() : '';
const transition = plTransitionEl ? (plTransitionEl.value || 'none') : 'none';
const showOverlay = plShowOverlayEl ? !!plShowOverlayEl.checked : false;
plSaveBtn.disabled = true;
try {
const [updatedPlaylists, updatedDesc] = await Promise.all([
postDisplayPlaylists(activePlDisplayId, ids),
postDisplayUpdate(activePlDisplayId, { description: desc, transition })
postDisplayUpdate(activePlDisplayId, { description: desc, transition, show_overlay: showOverlay })
]);
const newIds = (updatedPlaylists && updatedPlaylists.active_playlist_ids)
@@ -401,6 +415,11 @@
const newTransition = updatedDesc && typeof updatedDesc.transition === 'string' ? updatedDesc.transition : transition;
activePlButton.dataset.currentTransition = newTransition || 'none';
const newShowOverlay = updatedDesc && typeof updatedDesc.show_overlay !== 'undefined'
? !!updatedDesc.show_overlay
: showOverlay;
activePlButton.dataset.currentShowOverlay = newShowOverlay ? '1' : '0';
showToast('Display updated', 'text-bg-success');
refreshPreviewIframe(activePlDisplayId);
if (plModal) plModal.hide();

View File

@@ -90,6 +90,50 @@
</div>
</div>
<div class="card card-elevated mt-4">
<div class="card-header">
<h2 class="h5 mb-0">Overlay</h2>
</div>
<div class="card-body">
<div class="text-muted small mb-3">
Upload a <strong>16:9 PNG</strong> overlay. It will be rendered on top of the display content.
Transparent areas will show the content underneath.
</div>
{% if overlay_url %}
<div class="mb-3">
<div class="text-muted small mb-2">Current overlay:</div>
<div style="max-width: 520px; border: 1px solid rgba(0,0,0,0.15); border-radius: 8px; overflow: hidden;">
<img
src="{{ overlay_url }}"
alt="Company overlay"
style="display:block; width:100%; height:auto; background: repeating-linear-gradient(45deg, #eee 0 12px, #fff 12px 24px);"
/>
</div>
</div>
{% else %}
<div class="text-muted mb-3">No overlay uploaded.</div>
{% endif %}
<form method="post" action="{{ url_for('company.upload_company_overlay') }}" enctype="multipart/form-data" class="d-flex gap-2 flex-wrap align-items-end">
<div>
<label class="form-label">Upload overlay (PNG)</label>
<input class="form-control" type="file" name="overlay" accept="image/png" required />
<div class="form-text">Tip: export at 1920×1080 (or any 16:9 size).</div>
</div>
<div>
<button class="btn btn-brand" type="submit">Save overlay</button>
</div>
</form>
{% if overlay_url %}
<form method="post" action="{{ url_for('company.delete_company_overlay') }}" class="mt-3" onsubmit="return confirm('Remove the overlay?');">
<button class="btn btn-outline-danger" type="submit">Remove overlay</button>
</form>
{% endif %}
</div>
</div>
<div class="card card-elevated mt-4">
<div class="card-header">
<h2 class="h5 mb-0">Users</h2>

View File

@@ -8,6 +8,17 @@
html, body { height: 100%; width: 100%; margin: 0; background: #000; overflow: hidden; }
#stage { position: fixed; inset: 0; width: 100vw; height: 100vh; background: #000; }
/* Optional company overlay (transparent PNG) */
#overlay {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
z-index: 5;
object-fit: contain;
}
/* Slide transitions (applied by JS via classes) */
#stage .slide {
position: absolute;
@@ -99,9 +110,13 @@
</div>
</div>
<div id="stage"></div>
{% if overlay_url %}
<img id="overlay" src="{{ overlay_url }}" alt="Overlay" />
{% endif %}
<script>
const token = "{{ display.token }}";
const stage = document.getElementById('stage');
let overlayEl = document.getElementById('overlay');
const noticeEl = document.getElementById('notice');
const noticeTitleEl = document.getElementById('noticeTitle');
const noticeTextEl = document.getElementById('noticeText');
@@ -164,6 +179,38 @@
stage.innerHTML = '';
}
function setOverlaySrc(src) {
const val = (src || '').trim();
if (!val) {
if (overlayEl && overlayEl.parentNode) overlayEl.parentNode.removeChild(overlayEl);
overlayEl = null;
return;
}
if (!overlayEl) {
overlayEl = document.createElement('img');
overlayEl.id = 'overlay';
overlayEl.alt = 'Overlay';
document.body.appendChild(overlayEl);
}
// Cache-bust in preview mode so changes show up instantly.
if (isPreview) {
try {
const u = new URL(val, window.location.origin);
u.searchParams.set('_ts', String(Date.now()));
overlayEl.src = u.toString();
return;
} catch(e) {
// fallthrough
}
}
overlayEl.src = val;
}
// Initialize overlay from server-side render.
if (overlayEl && overlayEl.src) setOverlaySrc(overlayEl.src);
function setSlideContent(container, item) {
if (item.type === 'image') {
const el = document.createElement('img');
@@ -272,6 +319,7 @@
playlist = await fetchPlaylist();
idx = 0;
applyTransitionClass(getTransitionMode(playlist));
setOverlaySrc(playlist && playlist.overlay_src);
next();
} catch (e) {
clearStage();
@@ -300,6 +348,7 @@
const oldStr = JSON.stringify(playlist);
const newStr = JSON.stringify(newPlaylist);
playlist = newPlaylist;
setOverlaySrc(playlist && playlist.overlay_src);
if (oldStr !== newStr) {
idx = 0;
applyTransitionClass(getTransitionMode(playlist));