// De afspeellijst-editor -- verplaatst uit inline script, shaer-bqr.
//
// Servergegevens komen uit pageData(); interpolatie kan niet in een statisch
// bestand. Inline script wordt bovendien geweigerd zodra deze pagina via een
// link BINNEN de site binnenkomt (shaer-0i6).
import { pageData } from './lib.js';
/**
* De teksten van deze modal, uit het gegevensblok van partials/playlist-editor.ejs.
*
* EIGEN BLOK en niet pageData(): die pakt met querySelector er EEN, en deze
* modal hangt onder pagina's die er zelf al een hebben. Een tweede blok zou
* daar genegeerd worden.
*
* De terugval is bewust de sleutelnaam en niet de Nederlandse tekst: ontbreekt
* er iets, dan zie je DAT er iets ontbreekt in plaats van een pagina die er
* half vertaald uitziet en waarvan niemand merkt welke helft.
*/
let _T = null;
function T(sleutel) {
if (_T === null) {
try {
const el = document.querySelector('script[type="application/json"][data-playlist-editor-i18n]');
_T = el ? (JSON.parse(el.textContent) || {}) : {};
} catch (e) { _T = {}; }
}
return _T[sleutel] != null ? _T[sleutel] : sleutel;
}
(function() {
// Idempotency: if window.openPlaylistEditor already defined (multiple
// partial includes on a single page), skip re-binding.
if (typeof window.openPlaylistEditor === 'function') return;
const CSRF = pageData().csrf || '';
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({
'&':'&','<':'<','>':'>','"':'"',"'":'''
}[c]));
}
function fmtDur(sec) {
if (!sec) return '';
const m = Math.floor(sec / 60), s = sec % 60;
return `${m}:${String(s).padStart(2, '0')}`;
}
async function api(method, url, body) {
const opts = {
method, credentials: 'same-origin',
headers: { 'X-CSRF-Token': CSRF },
};
if (body !== undefined) {
opts.headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(body);
}
const r = await fetch(url, opts);
return r.json();
}
/**
* Public entry point: open the editor.
* opts: { mode: 'create'|'edit', id?, onSaved? }
* onSaved is called with { id, playlist } after a successful save.
*/
window.openPlaylistEditor = async function(opts) {
opts = opts || {};
const mode = opts.mode === 'edit' ? 'edit' : 'create';
const isEdit = mode === 'edit';
// Load all audio tracks for the picker
let tracks = [];
try {
const j = await api('GET', '/admin/playlists/api/tracks');
if (Array.isArray(j.tracks)) tracks = j.tracks;
} catch (e) {
alert(T('e_tracks'));
return;
}
if (!tracks.length) {
alert(T('e_geen_tracks'));
return;
}
// Existing playlist data when editing
let initial = { title: '', artist: '', year: '', cover: '', kind: 'album', release_date: '', mb_release_id: '', track_ids: [] };
if (isEdit && opts.id) {
try {
const j = await api('GET', '/admin/playlists/api/' + encodeURIComponent(opts.id));
if (j.ok) initial = { ...initial, ...j.playlist };
} catch (e) {}
}
// ── DOM ────────────────────────────────────────────────────────
const backdrop = document.createElement('div');
backdrop.className = 'pl-modal-backdrop';
backdrop.innerHTML = `
`;
document.body.appendChild(backdrop);
const $ = sel => backdrop.querySelector(sel);
const close = () => backdrop.remove();
backdrop.addEventListener('click', e => { if (e.target === backdrop) close(); });
$('.pl-modal-close').addEventListener('click', close);
$('#pli-cancel').addEventListener('click', close);
// Index tracks by id for fast lookup
const trackById = new Map(tracks.map(t => [t.id, t]));
let selected = (initial.track_ids || []).filter(id => trackById.has(id));
const titleEl = $('#pli-title');
const artistEl = $('#pli-artist');
const yearEl = $('#pli-year');
const kindEl = $('#pli-kind');
const releaseEl = $('#pli-release-date');
const mbReleaseEl = $('#pli-mb-release');
const coverEl = $('#pli-cover');
// Uitgavedatum en release-id horen bij een ALBUM, niet bij een
// afspeellijst -- dat is wat de keuze album/playlist betekent. Ze
// verdwijnen dus als je omschakelt, en de server maakt ze dan ook leeg;
// dit scherm is de uitleg, niet de bewaking.
// ALLEEN een album, en niet "alles behalve playlist". Dat stond er eerst, en
// met een derde soort erbij zou een mixtape ineens uitgavevelden tonen die
// de server toch weggooit -- een scherm dat iets anders belooft dan het doet.
const toonUitgave = () => {
const album = kindEl.value === 'album';
for (const el of backdrop.querySelectorAll('.pl-uitgave')) el.hidden = !album;
};
kindEl.addEventListener('change', toonUitgave);
toonUitgave();
const coverThumb = $('#pli-cover-thumb');
const saveBtn = $('#pli-save');
const selectedEl = $('#pli-selected');
const availEl = $('#pli-available');
const searchEl = $('#pli-search');
const countEl = $('#pli-count');
coverEl.addEventListener('input', () => {
const u = coverEl.value.trim();
coverThumb.innerHTML = u
? ``
: `🎨`;
});
// ── Cover file upload (werkt in create ÉN edit) ───────────
// We uploaden naar het generieke image-endpoint (/posts/upload-image,
// requireAuth) dat een /media/-URL teruggeeft — dat heeft GEEN playlist-id
// nodig, dus uploaden kan ook al vóór het aanmaken. De URL belandt in het
// cover-veld en wordt bij het opslaan met de playlist meegestuurd.
const coverFileInput = $('#pli-cover-file');
const coverPickBtn = $('#pli-cover-pick');
const coverStatus = $('#pli-cover-status');
if (coverFileInput && coverPickBtn) {
coverPickBtn.addEventListener('click', () => coverFileInput.click());
coverFileInput.addEventListener('change', async (e) => {
const file = e.target.files && e.target.files[0];
coverFileInput.value = '';
if (!file) return;
if (!/^image\//.test(file.type)) {
coverStatus.textContent = T('e_alleen_afb');
coverStatus.className = 'pl-cover-status is-error';
return;
}
coverStatus.textContent = T('bezig');
coverStatus.className = 'pl-cover-status';
const fd = new FormData();
fd.append('image', file);
try {
const r = await fetch('/posts/upload-image',
{ method: 'POST', body: fd, credentials: 'same-origin' }
);
const j = await r.json();
if (!r.ok || !j.url) throw new Error(j.error || T('e_upload'));
const url = j.url || '';
coverEl.value = url;
coverThumb.innerHTML = url
? ``
: `🎨`;
coverStatus.textContent = '✓ Geüpload';
coverStatus.className = 'pl-cover-status is-ok';
} catch (err) {
coverStatus.textContent = T('e_mislukt') + err.message;
coverStatus.className = 'pl-cover-status is-error';
}
});
}
function updateSaveBtn() {
saveBtn.disabled = !titleEl.value.trim() || selected.length === 0;
}
function renderSelected() {
countEl.textContent = selected.length;
if (selected.length === 0) {
selectedEl.innerHTML = '