Initial import
This commit is contained in:
@@ -24,7 +24,22 @@
|
||||
<tbody>
|
||||
{% for p in playlists %}
|
||||
<tr>
|
||||
<td><strong>{{ p.name }}</strong></td>
|
||||
<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">
|
||||
@@ -79,27 +94,17 @@
|
||||
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-legacy-playlist-id="{{ d.assigned_playlist_id or '' }}"
|
||||
data-active-playlist-ids="{{ d.display_playlists | map(attribute='playlist_id') | list | join(',') }}"
|
||||
>
|
||||
Select playlists
|
||||
Configure display
|
||||
</button>
|
||||
<div class="small text-muted">
|
||||
<span class="js-active-playlists-summary" data-display-id="{{ d.id }}">—</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ink btn-sm js-edit-desc"
|
||||
data-display-id="{{ d.id }}"
|
||||
data-display-name="{{ d.name }}"
|
||||
data-current-desc="{{ d.description or '' }}"
|
||||
>
|
||||
Edit description
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -122,36 +127,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit description modal -->
|
||||
<div class="modal fade" id="editDescModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editDescModalTitle">Edit description</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label class="form-label" for="editDescInput">Description</label>
|
||||
<textarea class="form-control" id="editDescInput" maxlength="200" rows="3" placeholder="Optional description (max 200 chars)"></textarea>
|
||||
<div class="form-text"><span id="editDescCount">0</span>/200</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="editDescSaveBtn">Save</button>
|
||||
</div>
|
||||
</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">Select playlists</h5>
|
||||
<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>
|
||||
<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>
|
||||
@@ -285,66 +291,6 @@
|
||||
return data.display;
|
||||
}
|
||||
|
||||
// Description modal
|
||||
const modalEl = document.getElementById('editDescModal');
|
||||
const modal = modalEl ? new bootstrap.Modal(modalEl) : null;
|
||||
const titleEl = document.getElementById('editDescModalTitle');
|
||||
const inputEl = document.getElementById('editDescInput');
|
||||
const countEl = document.getElementById('editDescCount');
|
||||
const saveBtn = document.getElementById('editDescSaveBtn');
|
||||
|
||||
let activeDisplayId = null;
|
||||
|
||||
function updateCount() {
|
||||
if (!inputEl || !countEl) return;
|
||||
countEl.textContent = String((inputEl.value || '').length);
|
||||
}
|
||||
if (inputEl) inputEl.addEventListener('input', updateCount);
|
||||
|
||||
document.querySelectorAll('.js-edit-desc').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
activeDisplayId = btn.dataset.displayId;
|
||||
const displayName = btn.dataset.displayName || 'Display';
|
||||
const currentDesc = btn.dataset.currentDesc || '';
|
||||
if (titleEl) titleEl.textContent = `Edit description — ${displayName}`;
|
||||
if (inputEl) inputEl.value = currentDesc;
|
||||
updateCount();
|
||||
if (modal) modal.show();
|
||||
});
|
||||
});
|
||||
|
||||
async function saveDescription() {
|
||||
if (!activeDisplayId || !inputEl) return;
|
||||
const desc = (inputEl.value || '').trim();
|
||||
saveBtn.disabled = true;
|
||||
try {
|
||||
const updated = await postDisplayUpdate(activeDisplayId, { description: desc });
|
||||
// Update visible description
|
||||
const descEl = document.querySelector(`.js-display-desc[data-display-id="${activeDisplayId}"]`);
|
||||
if (descEl) descEl.textContent = updated.description ? updated.description : '—';
|
||||
// Update button's stored value
|
||||
const btn = document.querySelector(`.js-edit-desc[data-display-id="${activeDisplayId}"]`);
|
||||
if (btn) btn.dataset.currentDesc = updated.description || '';
|
||||
showToast('Description saved', 'text-bg-success');
|
||||
if (modal) modal.hide();
|
||||
} catch (e) {
|
||||
showToast(e && e.message ? e.message : 'Save failed', 'text-bg-danger');
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
if (saveBtn) {
|
||||
saveBtn.addEventListener('click', saveDescription);
|
||||
}
|
||||
if (modalEl) {
|
||||
modalEl.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault();
|
||||
saveDescription();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Playlists modal
|
||||
const plModalEl = document.getElementById('editPlaylistsModal');
|
||||
const plModal = plModalEl ? new bootstrap.Modal(plModalEl) : null;
|
||||
@@ -352,9 +298,18 @@
|
||||
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');
|
||||
let activePlDisplayId = null;
|
||||
let activePlButton = null;
|
||||
|
||||
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 = '';
|
||||
@@ -401,7 +356,15 @@
|
||||
activePlDisplayId = btn.dataset.displayId;
|
||||
activePlButton = btn;
|
||||
const displayName = btn.dataset.displayName || 'Display';
|
||||
if (plTitleEl) plTitleEl.textContent = `Select playlists — ${displayName}`;
|
||||
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';
|
||||
|
||||
const selected = computeActiveIdsFromDataset(btn);
|
||||
renderPlaylistCheckboxes(selected);
|
||||
if (plHintEl) {
|
||||
@@ -414,13 +377,31 @@
|
||||
async function savePlaylists() {
|
||||
if (!activePlDisplayId || !activePlButton || !plSaveBtn) return;
|
||||
const ids = getSelectedPlaylistIdsFromModal();
|
||||
const desc = plDescInputEl ? (plDescInputEl.value || '').trim() : '';
|
||||
const transition = plTransitionEl ? (plTransitionEl.value || 'none') : 'none';
|
||||
plSaveBtn.disabled = true;
|
||||
try {
|
||||
const updated = await postDisplayPlaylists(activePlDisplayId, ids);
|
||||
const newIds = (updated && updated.active_playlist_ids) ? updated.active_playlist_ids : ids;
|
||||
const [updatedPlaylists, updatedDesc] = await Promise.all([
|
||||
postDisplayPlaylists(activePlDisplayId, ids),
|
||||
postDisplayUpdate(activePlDisplayId, { description: desc, transition })
|
||||
]);
|
||||
|
||||
const newIds = (updatedPlaylists && updatedPlaylists.active_playlist_ids)
|
||||
? updatedPlaylists.active_playlist_ids
|
||||
: ids;
|
||||
setActiveIdsOnButton(activePlButton, newIds);
|
||||
refreshActivePlaylistSummary(activePlDisplayId, newIds);
|
||||
showToast('Playlists saved', 'text-bg-success');
|
||||
|
||||
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';
|
||||
|
||||
showToast('Display updated', 'text-bg-success');
|
||||
refreshPreviewIframe(activePlDisplayId);
|
||||
if (plModal) plModal.hide();
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user