600 lines
28 KiB
HTML
600 lines
28 KiB
HTML
{% extends "base.html" %}
|
|
{% block content %}
|
|
<h1 class="page-title">Dashboard</h1>
|
|
|
|
<div class="row mt-4">
|
|
<div class="col-12">
|
|
<div class="card card-elevated">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h2 class="h5 mb-0">Playlists</h2>
|
|
<form method="post" action="{{ url_for('company.create_playlist') }}" class="d-flex gap-2">
|
|
<input class="form-control" name="name" placeholder="New playlist name" required />
|
|
<button class="btn btn-brand" type="submit">Add</button>
|
|
</form>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>Name</th>
|
|
<th class="text-end">Items</th>
|
|
<th class="text-end">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for p in playlists %}
|
|
<tr>
|
|
<td>
|
|
<strong>{{ p.name }}</strong>
|
|
{# Indicators: schedule + priority #}
|
|
{% set has_schedule = (p.schedule_start is not none) or (p.schedule_end is not none) %}
|
|
{% if has_schedule %}
|
|
{% set is_active = (not p.schedule_start or p.schedule_start <= now_utc) and (not p.schedule_end or now_utc <= p.schedule_end) %}
|
|
<span class="ms-2" title="Scheduled playlist" style="font-weight:700;">📅</span>
|
|
<span
|
|
class="ms-1 schedule-status-dot {{ 'active' if is_active else 'inactive' }}"
|
|
title="{{ 'Schedule active' if is_active else 'Schedule inactive' }}"
|
|
></span>
|
|
{% endif %}
|
|
{% if p.is_priority %}
|
|
<span class="ms-1" title="Priority playlist" style="color:#dc3545; font-weight:700;">❗</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="text-end">{{ p.items|length }}</td>
|
|
<td class="text-end">
|
|
<div class="d-inline-flex gap-2">
|
|
<a class="btn btn-ink btn-sm" href="{{ url_for('company.playlist_detail', playlist_id=p.id) }}">Open</a>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="3" class="text-muted">No playlists yet.</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-12 mt-4">
|
|
<div class="card card-elevated">
|
|
<div class="card-header">
|
|
<h2 class="h5 mb-0">Displays</h2>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
{% for d in displays %}
|
|
<div class="col-12 col-md-6 col-xl-4">
|
|
<div class="card display-gallery-card h-100">
|
|
<div class="display-preview">
|
|
<div
|
|
class="display-preview-scale"
|
|
style="width: 1000%; height: 1000%; transform: scale(0.1); transform-origin: top left;"
|
|
>
|
|
<iframe
|
|
title="Preview — {{ d.name }}"
|
|
data-display-id="{{ d.id }}"
|
|
src="{{ url_for('display.display_player', token=d.token) }}?preview=1"
|
|
loading="lazy"
|
|
referrerpolicy="no-referrer"
|
|
style="width: 100%; height: 100%; border: 0;"
|
|
></iframe>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body d-flex flex-column gap-2">
|
|
<div>
|
|
<div class="fw-bold">{{ d.name }}</div>
|
|
<div class="text-muted small js-display-desc" data-display-id="{{ d.id }}">
|
|
{{ d.description or "—" }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex flex-column gap-2 mt-auto">
|
|
{# Multi-playlist selector: button opens modal with playlist checkboxes #}
|
|
<div class="d-flex gap-2 align-items-center">
|
|
<button
|
|
type="button"
|
|
class="btn btn-ink btn-sm js-edit-playlists"
|
|
data-display-id="{{ d.id }}"
|
|
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-current-ticker-enabled="{{ '1' if d.ticker_enabled else '0' }}"
|
|
data-current-ticker-rss-url="{{ d.ticker_rss_url or '' }}"
|
|
data-current-ticker-color="{{ d.ticker_color or '' }}"
|
|
data-current-ticker-bg-color="{{ d.ticker_bg_color or '' }}"
|
|
data-current-ticker-bg-opacity="{{ d.ticker_bg_opacity or '' }}"
|
|
data-current-ticker-font-family="{{ d.ticker_font_family or '' }}"
|
|
data-current-ticker-font-size-px="{{ d.ticker_font_size_px or '' }}"
|
|
data-current-ticker-speed="{{ d.ticker_speed or '' }}"
|
|
data-legacy-playlist-id="{{ d.assigned_playlist_id or '' }}"
|
|
data-active-playlist-ids="{{ d.display_playlists | map(attribute='playlist_id') | list | join(',') }}"
|
|
>
|
|
Configure display
|
|
</button>
|
|
<div class="small text-muted">
|
|
<span class="js-active-playlists-summary" data-display-id="{{ d.id }}">—</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="col-12">
|
|
<div class="text-muted">No displays. Ask admin to add displays.</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toast notifications -->
|
|
<div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 1080">
|
|
<div id="companyToast" class="toast" role="alert" aria-live="polite" aria-atomic="true">
|
|
<div class="toast-body" id="companyToastBody">Saved</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit playlists modal -->
|
|
<div class="modal fade" id="editPlaylistsModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="editPlaylistsModalTitle">Configure display</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label" for="editPlaylistsDescInput">Description</label>
|
|
<textarea
|
|
class="form-control"
|
|
id="editPlaylistsDescInput"
|
|
maxlength="200"
|
|
rows="3"
|
|
placeholder="Optional description (max 200 chars)"
|
|
></textarea>
|
|
<div class="form-text"><span id="editPlaylistsDescCount">0</span>/200</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label" for="editPlaylistsTransitionSelect">Slide transition</label>
|
|
<select class="form-select" id="editPlaylistsTransitionSelect">
|
|
<option value="none">None</option>
|
|
<option value="fade">Fade</option>
|
|
<option value="slide">Slide</option>
|
|
</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>
|
|
|
|
<div class="card" style="border: 1px solid rgba(0,0,0,0.10);">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<div class="fw-bold">Ticker tape</div>
|
|
<div class="text-muted small">Scroll RSS headlines at the bottom of the display.</div>
|
|
</div>
|
|
<div class="form-check form-switch">
|
|
<input class="form-check-input" type="checkbox" id="editTickerEnabled" />
|
|
<label class="form-check-label" for="editTickerEnabled">Enabled</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3">
|
|
<label class="form-label" for="editTickerRssUrl">RSS feed URL</label>
|
|
<input class="form-control" id="editTickerRssUrl" type="url" placeholder="https://example.com/feed.xml" />
|
|
<div class="form-text">Tip: use a public RSS/Atom feed. Headlines are fetched server-side.</div>
|
|
</div>
|
|
|
|
<div class="row g-2 mt-2">
|
|
<div class="col-12 col-md-5">
|
|
<label class="form-label" for="editTickerColor">Text color</label>
|
|
<input class="form-control form-control-color" id="editTickerColor" type="color" value="#ffffff" title="Choose text color" />
|
|
</div>
|
|
<div class="col-12 col-md-5">
|
|
<label class="form-label" for="editTickerFontSize">Font size (px)</label>
|
|
<input class="form-control" id="editTickerFontSize" type="number" min="10" max="200" step="1" placeholder="28" />
|
|
</div>
|
|
<div class="col-12 col-md-7">
|
|
<label class="form-label" for="editTickerFontFamily">Font</label>
|
|
<select class="form-select" id="editTickerFontFamily">
|
|
<option value="system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif">System (default)</option>
|
|
<option value="Arial, Helvetica, sans-serif">Arial</option>
|
|
<option value="Segoe UI, Arial, sans-serif">Segoe UI</option>
|
|
<option value="Roboto, Arial, sans-serif">Roboto</option>
|
|
<option value="Georgia, serif">Georgia</option>
|
|
<option value="Times New Roman, Times, serif">Times New Roman</option>
|
|
<option value="ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace">Monospace</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-12 col-md-7">
|
|
<label class="form-label" for="editTickerBgColor">Background color</label>
|
|
<input class="form-control form-control-color" id="editTickerBgColor" type="color" value="#000000" title="Choose background color" />
|
|
</div>
|
|
<div class="col-12 col-md-5">
|
|
<label class="form-label" for="editTickerBgOpacity">Background opacity</label>
|
|
<input class="form-range" id="editTickerBgOpacity" type="range" min="0" max="100" step="1" />
|
|
<div class="form-text"><span id="editTickerBgOpacityLabel">75</span>%</div>
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label" for="editTickerSpeed">Speed</label>
|
|
<input class="form-range" id="editTickerSpeed" type="range" min="1" max="100" step="1" />
|
|
<div class="form-text">Slower ⟷ Faster</div>
|
|
</div>
|
|
</div>
|
|
</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>
|
|
<div class="form-text mt-2" id="editPlaylistsHint"></div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-brand" id="editPlaylistsSaveBtn">Save</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# Embed playlists list as JSON to avoid templating inside JS (keeps JS linters happy). #}
|
|
<script type="application/json" id="allPlaylistsJson">{{ playlists_json|tojson }}</script>
|
|
{% endblock %}
|
|
|
|
{% block page_scripts %}
|
|
<script>
|
|
(function () {
|
|
let ALL_PLAYLISTS = [];
|
|
try {
|
|
const el = document.getElementById('allPlaylistsJson');
|
|
ALL_PLAYLISTS = el ? JSON.parse(el.textContent || '[]') : [];
|
|
} catch (e) {
|
|
ALL_PLAYLISTS = [];
|
|
}
|
|
|
|
const toastEl = document.getElementById('companyToast');
|
|
const toastBodyEl = document.getElementById('companyToastBody');
|
|
const toast = toastEl ? new bootstrap.Toast(toastEl, { delay: 2200 }) : null;
|
|
|
|
function showToast(message, variant) {
|
|
if (!toast || !toastEl || !toastBodyEl) return;
|
|
toastEl.classList.remove('text-bg-success', 'text-bg-danger', 'text-bg-secondary');
|
|
if (variant) toastEl.classList.add(variant);
|
|
toastBodyEl.textContent = message;
|
|
toast.show();
|
|
}
|
|
|
|
async function postDisplayUpdate(displayId, payload) {
|
|
const res = await fetch(`/company/displays/${displayId}` , {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
},
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const data = await res.json().catch(() => null);
|
|
if (!res.ok || !data || !data.ok) {
|
|
const msg = (data && data.error) ? data.error : 'Save failed';
|
|
throw new Error(msg);
|
|
}
|
|
return data.display;
|
|
}
|
|
|
|
function refreshPreviewIframe(displayId) {
|
|
const iframe = document.querySelector(`iframe[data-display-id="${displayId}"]`);
|
|
if (!iframe || !iframe.src) return;
|
|
try {
|
|
const u = new URL(iframe.src, window.location.origin);
|
|
// Ensure preview flag is present (and bust cache).
|
|
u.searchParams.set('preview', '1');
|
|
u.searchParams.set('_ts', String(Date.now()));
|
|
iframe.src = u.toString();
|
|
} catch (e) {
|
|
// Fallback: naive cache buster
|
|
const sep = iframe.src.includes('?') ? '&' : '?';
|
|
iframe.src = `${iframe.src}${sep}_ts=${Date.now()}`;
|
|
}
|
|
}
|
|
|
|
function parseIds(csv) {
|
|
const s = (csv || '').trim();
|
|
if (!s) return [];
|
|
return s.split(',').map(x => parseInt(x, 10)).filter(n => Number.isFinite(n));
|
|
}
|
|
|
|
function computeActiveIdsFromDataset(btn) {
|
|
// If display_playlist table has rows, we use that.
|
|
// Otherwise fall back to legacy single playlist assignment.
|
|
const active = parseIds(btn.dataset.activePlaylistIds);
|
|
if (active.length) return active;
|
|
const legacy = parseInt(btn.dataset.legacyPlaylistId || '', 10);
|
|
return Number.isFinite(legacy) ? [legacy] : [];
|
|
}
|
|
|
|
function setActiveIdsOnButton(btn, ids) {
|
|
btn.dataset.activePlaylistIds = (ids || []).join(',');
|
|
}
|
|
|
|
function playlistNameById(id) {
|
|
const p = (ALL_PLAYLISTS || []).find(x => x.id === id);
|
|
return p ? p.name : null;
|
|
}
|
|
|
|
function refreshActivePlaylistSummary(displayId, ids) {
|
|
const el = document.querySelector(`.js-active-playlists-summary[data-display-id="${displayId}"]`);
|
|
if (!el) return;
|
|
if (!ids || ids.length === 0) {
|
|
el.textContent = '(none)';
|
|
return;
|
|
}
|
|
const names = ids.map(playlistNameById).filter(Boolean);
|
|
el.textContent = names.length ? names.join(', ') : `${ids.length} selected`;
|
|
}
|
|
|
|
// Initialize summary labels on page load.
|
|
document.querySelectorAll('.js-edit-playlists').forEach((btn) => {
|
|
const displayId = btn.dataset.displayId;
|
|
const ids = computeActiveIdsFromDataset(btn);
|
|
refreshActivePlaylistSummary(displayId, ids);
|
|
});
|
|
|
|
async function postDisplayPlaylists(displayId, playlistIds) {
|
|
const res = await fetch(`/company/displays/${displayId}/playlists`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Accept': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
},
|
|
body: JSON.stringify({ playlist_ids: playlistIds })
|
|
});
|
|
const data = await res.json().catch(() => null);
|
|
if (!res.ok || !data || !data.ok) {
|
|
const msg = (data && data.error) ? data.error : 'Save failed';
|
|
throw new Error(msg);
|
|
}
|
|
return data.display;
|
|
}
|
|
|
|
// Playlists modal
|
|
const plModalEl = document.getElementById('editPlaylistsModal');
|
|
const plModal = plModalEl ? new bootstrap.Modal(plModalEl) : null;
|
|
const plTitleEl = document.getElementById('editPlaylistsModalTitle');
|
|
const plListEl = document.getElementById('editPlaylistsList');
|
|
const plHintEl = document.getElementById('editPlaylistsHint');
|
|
const plSaveBtn = document.getElementById('editPlaylistsSaveBtn');
|
|
const plDescInputEl = document.getElementById('editPlaylistsDescInput');
|
|
const plDescCountEl = document.getElementById('editPlaylistsDescCount');
|
|
const plTransitionEl = document.getElementById('editPlaylistsTransitionSelect');
|
|
const plShowOverlayEl = document.getElementById('editPlaylistsShowOverlayCheck');
|
|
const tickerEnabledEl = document.getElementById('editTickerEnabled');
|
|
const tickerRssUrlEl = document.getElementById('editTickerRssUrl');
|
|
const tickerColorEl = document.getElementById('editTickerColor');
|
|
const tickerBgColorEl = document.getElementById('editTickerBgColor');
|
|
const tickerBgOpacityEl = document.getElementById('editTickerBgOpacity');
|
|
const tickerBgOpacityLabelEl = document.getElementById('editTickerBgOpacityLabel');
|
|
const tickerFontFamilyEl = document.getElementById('editTickerFontFamily');
|
|
const tickerFontSizeEl = document.getElementById('editTickerFontSize');
|
|
const tickerSpeedEl = document.getElementById('editTickerSpeed');
|
|
let activePlDisplayId = null;
|
|
let activePlButton = null;
|
|
|
|
function setRangeValue(rangeEl, labelEl, value, fallback) {
|
|
if (!rangeEl) return;
|
|
const n = parseInt(value || '', 10);
|
|
const v = Number.isFinite(n) ? n : fallback;
|
|
rangeEl.value = String(v);
|
|
if (labelEl) labelEl.textContent = String(v);
|
|
}
|
|
|
|
function onOpacityInput() {
|
|
if (!tickerBgOpacityEl || !tickerBgOpacityLabelEl) return;
|
|
tickerBgOpacityLabelEl.textContent = String(tickerBgOpacityEl.value || '0');
|
|
}
|
|
if (tickerBgOpacityEl) tickerBgOpacityEl.addEventListener('input', onOpacityInput);
|
|
|
|
function updatePlDescCount() {
|
|
if (!plDescInputEl || !plDescCountEl) return;
|
|
plDescCountEl.textContent = String((plDescInputEl.value || '').length);
|
|
}
|
|
if (plDescInputEl) plDescInputEl.addEventListener('input', updatePlDescCount);
|
|
|
|
function renderPlaylistCheckboxes(selectedIds) {
|
|
if (!plListEl) return;
|
|
plListEl.innerHTML = '';
|
|
const selectedSet = new Set(selectedIds || []);
|
|
const pls = (ALL_PLAYLISTS || []).slice().sort((a,b) => (a.name || '').localeCompare(b.name || ''));
|
|
if (pls.length === 0) {
|
|
plListEl.innerHTML = '<div class="text-muted">No playlists available.</div>';
|
|
return;
|
|
}
|
|
|
|
pls.forEach((p) => {
|
|
const id = `pl_cb_${p.id}`;
|
|
const row = document.createElement('div');
|
|
row.className = 'form-check';
|
|
|
|
const input = document.createElement('input');
|
|
input.className = 'form-check-input';
|
|
input.type = 'checkbox';
|
|
input.id = id;
|
|
input.value = String(p.id);
|
|
input.checked = selectedSet.has(p.id);
|
|
|
|
const label = document.createElement('label');
|
|
label.className = 'form-check-label';
|
|
label.setAttribute('for', id);
|
|
label.textContent = p.name;
|
|
|
|
row.appendChild(input);
|
|
row.appendChild(label);
|
|
plListEl.appendChild(row);
|
|
});
|
|
}
|
|
|
|
function getSelectedPlaylistIdsFromModal() {
|
|
if (!plListEl) return [];
|
|
return Array.from(plListEl.querySelectorAll('input[type="checkbox"]'))
|
|
.filter(cb => cb.checked)
|
|
.map(cb => parseInt(cb.value, 10))
|
|
.filter(n => Number.isFinite(n));
|
|
}
|
|
|
|
document.querySelectorAll('.js-edit-playlists').forEach((btn) => {
|
|
btn.addEventListener('click', () => {
|
|
activePlDisplayId = btn.dataset.displayId;
|
|
activePlButton = btn;
|
|
const displayName = btn.dataset.displayName || 'Display';
|
|
if (plTitleEl) plTitleEl.textContent = `Configure display — ${displayName}`;
|
|
|
|
const currentDesc = btn.dataset.currentDesc || '';
|
|
if (plDescInputEl) plDescInputEl.value = currentDesc;
|
|
updatePlDescCount();
|
|
|
|
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';
|
|
}
|
|
|
|
if (tickerEnabledEl) {
|
|
const raw = (btn.dataset.currentTickerEnabled || '').toLowerCase();
|
|
tickerEnabledEl.checked = raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
|
|
}
|
|
if (tickerRssUrlEl) tickerRssUrlEl.value = btn.dataset.currentTickerRssUrl || '';
|
|
if (tickerColorEl) tickerColorEl.value = btn.dataset.currentTickerColor || '#ffffff';
|
|
if (tickerBgColorEl) tickerBgColorEl.value = btn.dataset.currentTickerBgColor || '#000000';
|
|
setRangeValue(tickerBgOpacityEl, tickerBgOpacityLabelEl, btn.dataset.currentTickerBgOpacity, 75);
|
|
onOpacityInput();
|
|
if (tickerFontFamilyEl) tickerFontFamilyEl.value = btn.dataset.currentTickerFontFamily || 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif';
|
|
if (tickerFontSizeEl) tickerFontSizeEl.value = btn.dataset.currentTickerFontSizePx || '';
|
|
setRangeValue(tickerSpeedEl, null, btn.dataset.currentTickerSpeed, 25);
|
|
|
|
const selected = computeActiveIdsFromDataset(btn);
|
|
renderPlaylistCheckboxes(selected);
|
|
if (plHintEl) {
|
|
plHintEl.textContent = selected.length ? `${selected.length} currently selected.` : 'No playlists currently selected.';
|
|
}
|
|
if (plModal) plModal.show();
|
|
});
|
|
});
|
|
|
|
async function savePlaylists() {
|
|
if (!activePlDisplayId || !activePlButton || !plSaveBtn) return;
|
|
const ids = getSelectedPlaylistIdsFromModal();
|
|
const desc = plDescInputEl ? (plDescInputEl.value || '').trim() : '';
|
|
const transition = plTransitionEl ? (plTransitionEl.value || 'none') : 'none';
|
|
const showOverlay = plShowOverlayEl ? !!plShowOverlayEl.checked : false;
|
|
const tickerEnabled = tickerEnabledEl ? !!tickerEnabledEl.checked : false;
|
|
const tickerRssUrl = tickerRssUrlEl ? (tickerRssUrlEl.value || '').trim() : '';
|
|
const tickerColor = tickerColorEl ? (tickerColorEl.value || '').trim() : '';
|
|
const tickerBgColor = tickerBgColorEl ? (tickerBgColorEl.value || '').trim() : '';
|
|
const tickerBgOpacity = tickerBgOpacityEl ? (tickerBgOpacityEl.value || '').trim() : '';
|
|
const tickerFontFamily = tickerFontFamilyEl ? (tickerFontFamilyEl.value || '').trim() : '';
|
|
const tickerFontSizePx = tickerFontSizeEl ? (tickerFontSizeEl.value || '').trim() : '';
|
|
const tickerSpeed = tickerSpeedEl ? (tickerSpeedEl.value || '').trim() : '';
|
|
plSaveBtn.disabled = true;
|
|
try {
|
|
const [updatedPlaylists, updatedDesc] = await Promise.all([
|
|
postDisplayPlaylists(activePlDisplayId, ids),
|
|
postDisplayUpdate(activePlDisplayId, {
|
|
description: desc,
|
|
transition,
|
|
show_overlay: showOverlay,
|
|
ticker_enabled: tickerEnabled,
|
|
ticker_rss_url: tickerRssUrl,
|
|
ticker_color: tickerColor,
|
|
ticker_bg_color: tickerBgColor,
|
|
ticker_bg_opacity: tickerBgOpacity,
|
|
ticker_font_family: tickerFontFamily,
|
|
ticker_font_size_px: tickerFontSizePx,
|
|
ticker_speed: tickerSpeed
|
|
})
|
|
]);
|
|
|
|
const newIds = (updatedPlaylists && updatedPlaylists.active_playlist_ids)
|
|
? updatedPlaylists.active_playlist_ids
|
|
: ids;
|
|
setActiveIdsOnButton(activePlButton, newIds);
|
|
refreshActivePlaylistSummary(activePlDisplayId, newIds);
|
|
|
|
const descEl = document.querySelector(`.js-display-desc[data-display-id="${activePlDisplayId}"]`);
|
|
const newDesc = updatedDesc && typeof updatedDesc.description === 'string' ? updatedDesc.description : desc;
|
|
if (descEl) descEl.textContent = newDesc ? newDesc : '—';
|
|
activePlButton.dataset.currentDesc = newDesc || '';
|
|
|
|
// Keep button dataset in sync so reopening modal shows correct value.
|
|
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';
|
|
|
|
const newTickerEnabled = updatedDesc && typeof updatedDesc.ticker_enabled !== 'undefined'
|
|
? !!updatedDesc.ticker_enabled
|
|
: tickerEnabled;
|
|
activePlButton.dataset.currentTickerEnabled = newTickerEnabled ? '1' : '0';
|
|
|
|
activePlButton.dataset.currentTickerRssUrl = (updatedDesc && typeof updatedDesc.ticker_rss_url === 'string')
|
|
? (updatedDesc.ticker_rss_url || '')
|
|
: tickerRssUrl;
|
|
|
|
activePlButton.dataset.currentTickerColor = (updatedDesc && typeof updatedDesc.ticker_color === 'string')
|
|
? (updatedDesc.ticker_color || '')
|
|
: tickerColor;
|
|
|
|
activePlButton.dataset.currentTickerBgColor = (updatedDesc && typeof updatedDesc.ticker_bg_color === 'string')
|
|
? (updatedDesc.ticker_bg_color || '')
|
|
: tickerBgColor;
|
|
|
|
activePlButton.dataset.currentTickerBgOpacity = (updatedDesc && (typeof updatedDesc.ticker_bg_opacity === 'number' || typeof updatedDesc.ticker_bg_opacity === 'string'))
|
|
? String(updatedDesc.ticker_bg_opacity || '')
|
|
: String(tickerBgOpacity || '');
|
|
|
|
activePlButton.dataset.currentTickerFontFamily = (updatedDesc && typeof updatedDesc.ticker_font_family === 'string')
|
|
? (updatedDesc.ticker_font_family || '')
|
|
: tickerFontFamily;
|
|
|
|
activePlButton.dataset.currentTickerFontSizePx = (updatedDesc && (typeof updatedDesc.ticker_font_size_px === 'number' || typeof updatedDesc.ticker_font_size_px === 'string'))
|
|
? String(updatedDesc.ticker_font_size_px || '')
|
|
: String(tickerFontSizePx || '');
|
|
|
|
activePlButton.dataset.currentTickerSpeed = (updatedDesc && (typeof updatedDesc.ticker_speed === 'number' || typeof updatedDesc.ticker_speed === 'string'))
|
|
? String(updatedDesc.ticker_speed || '')
|
|
: String(tickerSpeed || '');
|
|
|
|
showToast('Display updated', 'text-bg-success');
|
|
refreshPreviewIframe(activePlDisplayId);
|
|
if (plModal) plModal.hide();
|
|
} catch (e) {
|
|
showToast(e && e.message ? e.message : 'Save failed', 'text-bg-danger');
|
|
} finally {
|
|
plSaveBtn.disabled = false;
|
|
}
|
|
}
|
|
|
|
if (plSaveBtn) {
|
|
plSaveBtn.addEventListener('click', savePlaylists);
|
|
}
|
|
})();
|
|
</script>
|
|
{% endblock %}
|