Update app
This commit is contained in:
@@ -72,16 +72,22 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column gap-2 mt-auto">
|
||||
<select
|
||||
class="form-select form-select-sm js-playlist-select"
|
||||
data-display-id="{{ d.id }}"
|
||||
aria-label="Playlist selection"
|
||||
>
|
||||
<option value="">(none)</option>
|
||||
{% for p in playlists %}
|
||||
<option value="{{ p.id }}" {% if d.assigned_playlist_id == p.id %}selected{% endif %}>{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{# 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-legacy-playlist-id="{{ d.assigned_playlist_id or '' }}"
|
||||
data-active-playlist-ids="{{ d.display_playlists | map(attribute='playlist_id') | list | join(',') }}"
|
||||
>
|
||||
Select playlists
|
||||
</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
|
||||
@@ -136,11 +142,43 @@
|
||||
</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>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<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;
|
||||
@@ -187,24 +225,66 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Playlist auto-save
|
||||
document.querySelectorAll('.js-playlist-select').forEach((sel) => {
|
||||
sel.addEventListener('change', async () => {
|
||||
const displayId = sel.dataset.displayId;
|
||||
const playlistId = sel.value || null;
|
||||
sel.disabled = true;
|
||||
try {
|
||||
await postDisplayUpdate(displayId, { playlist_id: playlistId });
|
||||
showToast('Playlist saved', 'text-bg-success');
|
||||
refreshPreviewIframe(displayId);
|
||||
} catch (e) {
|
||||
showToast(e && e.message ? e.message : 'Save failed', 'text-bg-danger');
|
||||
} finally {
|
||||
sel.disabled = false;
|
||||
}
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
// Description modal
|
||||
const modalEl = document.getElementById('editDescModal');
|
||||
const modal = modalEl ? new bootstrap.Modal(modalEl) : null;
|
||||
@@ -264,6 +344,95 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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');
|
||||
let activePlDisplayId = null;
|
||||
let activePlButton = null;
|
||||
|
||||
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 = `Select playlists — ${displayName}`;
|
||||
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();
|
||||
plSaveBtn.disabled = true;
|
||||
try {
|
||||
const updated = await postDisplayPlaylists(activePlDisplayId, ids);
|
||||
const newIds = (updated && updated.active_playlist_ids) ? updated.active_playlist_ids : ids;
|
||||
setActiveIdsOnButton(activePlButton, newIds);
|
||||
refreshActivePlaylistSummary(activePlDisplayId, newIds);
|
||||
showToast('Playlists saved', '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 %}
|
||||
|
||||
@@ -369,6 +369,7 @@
|
||||
|
||||
<script type="module">
|
||||
(function() {
|
||||
// Keep the card layout in ONE place to ensure newly-added items match server-rendered items.
|
||||
// -------------------------
|
||||
// Add-item modal + steps
|
||||
// -------------------------
|
||||
@@ -880,7 +881,7 @@
|
||||
<strong>#${i.position}</strong>
|
||||
${badge}
|
||||
</div>
|
||||
${safeTitle ? `<div class="small">${safeTitle}</div>` : ''}
|
||||
${safeTitle ? `<div class="small">${safeTitle}</div>` : `<div class="small">.</div>`}
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="${deleteAction}" onsubmit="return confirm('Delete item?');">
|
||||
@@ -888,11 +889,11 @@
|
||||
</form>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="thumb">${thumb}</div>
|
||||
<div class="text-muted small d-flex align-items-center gap-2 flex-wrap">
|
||||
<!-- Intentionally do NOT show file names or URLs for privacy/clean UI -->
|
||||
${durationInput}
|
||||
</div>
|
||||
<div class="thumb">${thumb}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user