import { S } from '../state.js'; import { esc, toast } from '../utils.js'; import { api, coverUrl, ndAuth, invalidateApiCache } from '../api.js'; // Circular — safe (only used inside function bodies) import { playQueue, dlSongLocal, nativeInsertAt, renderQueue } from '../player/player.js'; import { navigate, loadPlaylists, loadPlaylistDetail, renderSidebarPlaylists, bustSidebarCover } from '../views/views.js'; // ─── CONTEXT MENU ─── export function showCtxMenu(e, song, list, idx) { const menu = document.getElementById('ctx-menu'); menu.innerHTML = ''; const items = [ { icon: '▶', label: 'Abspielen', fn: () => playQueue(list, idx) }, { icon: '+', label: 'Als Nächstes einfügen', fn: () => { const insertIdx = S.qIdx + 1; S.queue.splice(insertIdx, 0, {...song}); nativeInsertAt(song, insertIdx); renderQueue(); toast('In Warteschlange eingefügt'); }}, { icon: '⬇', label: 'Am Ende der Queue', fn: () => { S.queue.push({...song}); nativeInsertAt(song, -1); renderQueue(); toast('Zur Queue hinzugefügt'); }}, { sep: true }, { icon: '🎶', label: 'Zu Playlist hinzufügen →', submenu: true, fn: () => openPlModal([song]) }, { icon: '⭐', label: song._starred ? 'Aus Favoriten entfernen' : 'Zu Favoriten', fn: () => { api(song._starred ? 'unstar' : 'star', `&id=${song.id}`) .then(() => { song._starred = !song._starred; toast(song._starred ? '⭐ Favorit hinzugefügt' : 'Favorit entfernt'); }) .catch(err => toast('Fehler: ' + err.message)); }}, { sep: true }, { icon: '💿', label: 'Zum Album', fn: () => song.albumId && navigate('album-detail', { id: song.albumId, name: song.album||'', artist: song.artist||'', artistId: song.artistId||'', coverArt: song.coverArt||'' }) }, { icon: '🔗', label: 'Teilen', fn: () => import('./share.js').then(m => m.shareSong(song)) }, { icon: 'ℹ', label: 'Song-Info', fn: () => import('./metadata.js').then(m => m.showSongInfo(song)) }, { icon: '⬇', label: 'Herunterladen', fn: () => dlSongLocal(song) }, ]; items.forEach(item => { if(item.sep) { const d=document.createElement('div'); d.className='ctx-separator'; menu.appendChild(d); return; } const el = document.createElement('div'); el.className = 'ctx-item'; el.innerHTML = `${item.icon}${item.label}`; el.onclick = () => { hideCtxMenu(); item.fn(); }; menu.appendChild(el); }); const vw=window.innerWidth, vh=window.innerHeight; let x=e.clientX, y=e.clientY; menu.style.left='-9999px'; menu.style.top='-9999px'; menu.style.display='block'; const w=menu.offsetWidth, h=menu.offsetHeight; if(x+w>vw-8) x=vw-w-8; if(y+h>vh-8) y=vh-h-8; menu.style.left=x+'px'; menu.style.top=y+'px'; } export function hideCtxMenu() { const m = document.getElementById('ctx-menu'); if (!m) return; m.style.display='none'; m.innerHTML=''; } document.addEventListener('click', e => { const m = document.getElementById('ctx-menu'); if (m && !m.contains(e.target)) hideCtxMenu(); }); document.addEventListener('scroll', () => { const m = document.getElementById('ctx-menu'); if (m && m.style.display !== 'none') hideCtxMenu(); }, { passive: true, capture: true }); document.addEventListener('touchmove', () => { const m = document.getElementById('ctx-menu'); if (m && m.style.display !== 'none') hideCtxMenu(); }, { passive: true, capture: true }); document.addEventListener('keydown', e => { if(e.key==='Escape') hideCtxMenu(); }); // ─── PLAYLIST MODAL (Add to / Create) ─── export async function openPlModal(songs) { S.plModalSongs = songs; document.getElementById('pl-modal-title').textContent = songs.length===1 ? `"${songs[0].title}" zu Playlist` : `${songs.length} Songs zu Playlist`; document.getElementById('pl-new-name').value = ''; const list = document.getElementById('pl-list'); list.innerHTML = '
Lädt…
'; document.getElementById('pl-modal').classList.add('open'); try { const r = await api('getPlaylists'); S.cachedPlaylists = r.playlists?.playlist || []; list.innerHTML = ''; if(!S.cachedPlaylists.length) { list.innerHTML = '
Noch keine Playlists vorhanden
'; return; } S.cachedPlaylists.forEach(pl => { const row = document.createElement('div'); row.className = 'pl-row'; const cv = pl.coverArt ? coverUrl(pl.coverArt, 96) : null; const inner = cv ? `` : ''; row.innerHTML = `${inner} ${esc(pl.name)}${pl.songCount} Titel`; row.onclick = () => addSongsToPlaylist(pl.id, pl.name); list.appendChild(row); }); } catch(e) { list.innerHTML = `
${e.message}
`; } } export function closePlModal() { document.getElementById('pl-modal').classList.remove('open'); S.plModalSongs = []; } export async function addSongsToPlaylist(plId, plName) { const songs = [...S.plModalSongs]; closePlModal(); try { const ids = songs.map(s => `&songIdToAdd=${s.id}`).join(''); await api('updatePlaylist', `&playlistId=${plId}${ids}`); toast(`✅ ${songs.length===1 ? songs[0].title : songs.length+' Songs'} → ${plName}`); } catch(e) { toast('Fehler: ' + e.message); } } export async function createAndAddPlaylist() { const name = document.getElementById('pl-new-name').value.trim(); if(!name) { document.getElementById('pl-new-name').focus(); return; } closePlModal(); try { const ids = S.plModalSongs.map(s => `&songId=${s.id}`).join(''); await api('createPlaylist', `&name=${encodeURIComponent(name)}${ids}`); toast(`✅ Playlist "${name}" erstellt`); invalidateApiCache('getPlaylists'); // sonst zeigt die Seitenleiste die neue Liste 3 Min. nicht renderSidebarPlaylists(); if(document.getElementById('page-playlists').classList.contains('active')) loadPlaylists(); } catch(e) { toast('Fehler: ' + e.message); } } export function createPlaylistPrompt() { document.getElementById('pl-new-name').value = ''; S.plModalSongs = []; document.getElementById('pl-modal-title').textContent = 'Neue Playlist erstellen'; document.getElementById('pl-list').innerHTML = '
Füge Songs später per Rechtsklick hinzu.
'; document.getElementById('pl-modal').classList.add('open'); } document.addEventListener('click', e => { const m = document.getElementById('pl-modal'); if(m.classList.contains('open') && e.target===m) closePlModal(); }); // ─── CONFIRM DIALOG ─── let _confirmResolve = null; export function _confirmOk() { document.getElementById('confirm-overlay').classList.remove('open'); _confirmResolve?.(true); _confirmResolve = null; } export function _confirmCancel() { document.getElementById('confirm-overlay').classList.remove('open'); _confirmResolve?.(false); _confirmResolve = null; } export function showConfirm(title, msg, okLabel = 'Bestätigen') { document.getElementById('confirm-title').textContent = title; document.getElementById('confirm-msg').textContent = msg; document.getElementById('confirm-ok-btn').textContent = okLabel; document.getElementById('confirm-overlay').classList.add('open'); return new Promise(resolve => { _confirmResolve = resolve; }); } // ─── EDIT PLAYLIST MODAL ─── export function openEditPlModal(pl) { const overlay = document.getElementById('edit-pl-overlay'); document.getElementById('edit-pl-name').value = pl.name || ''; document.getElementById('edit-pl-desc').value = pl.comment || ''; const tog = document.getElementById('edit-pl-public-toggle'); tog.classList.toggle('on', !!pl.public); const cv = coverUrl(pl.coverArt, 300); document.getElementById('edit-pl-cover').innerHTML = cv ? `` : '
🎶
'; overlay.classList.add('open'); } export function closeEditPlModal() { document.getElementById('edit-pl-overlay').classList.remove('open'); } export async function saveEditPlaylist() { const pl = S._currentPlaylist; if(!pl) return; const name = document.getElementById('edit-pl-name').value.trim(); if(!name) { toast('Name darf nicht leer sein'); return; } const comment = document.getElementById('edit-pl-desc').value.trim(); const pub = document.getElementById('edit-pl-public-toggle').classList.contains('on'); const btn = document.querySelector('.edit-pl-btn-save'); btn.disabled=true; btn.textContent='Speichern…'; try { await api('updatePlaylist', `&playlistId=${pl.id}&name=${encodeURIComponent(name)}&comment=${encodeURIComponent(comment)}&public=${pub}`); S._currentPlaylist = {...S._currentPlaylist, name, comment, public:pub}; document.getElementById('playlist-detail-name').textContent = name; const descEl = document.getElementById('playlist-detail-desc'); if(descEl) { descEl.textContent=comment; descEl.style.display=comment?'':'none'; } closeEditPlModal(); toast('Playlist aktualisiert'); invalidateApiCache('getPlaylists'); // Name/Sichtbarkeit stehen auch in der Seitenleiste renderSidebarPlaylists(); } catch(e) { toast('Fehler: '+e.message); } finally { btn.disabled=false; btn.textContent='Speichern'; } } export function editPlUploadCover() { document.getElementById('edit-pl-file').click(); } export async function editPlCoverSelected(input) { const file = input.files?.[0]; if(!file || !S._currentPlaylist) return; input.value = ''; const preview = URL.createObjectURL(file); document.getElementById('edit-pl-cover').innerHTML = ``; try { const token = await ndAuth(); const form = new FormData(); form.append('image', file, file.name); const res = await fetch(`${S.cfg.url}/api/playlist/${S._currentPlaylist.id}/image`, { method: 'POST', headers: {'X-Nd-Authorization': 'Bearer '+token}, body: form, }); if(!res.ok) throw new Error(res.status); toast('Cover aktualisiert'); setTimeout(() => { const bust = coverUrl(S._currentPlaylist.coverArt, 300) + '&_cb=' + Date.now(); const detailImg = document.getElementById('playlist-cover'); if(detailImg) detailImg.innerHTML = ``; document.getElementById('edit-pl-cover').innerHTML = ``; bustSidebarCover(S._currentPlaylist.id, S._currentPlaylist.coverArt); }, 800); } catch(e) { S._ndToken = null; toast('Cover-Upload fehlgeschlagen: '+e.message); const cv = coverUrl(S._currentPlaylist.coverArt, 300); document.getElementById('edit-pl-cover').innerHTML = cv ? `` : '
🎶
'; } } export async function editPlRemoveCover() { if(!S._currentPlaylist) return; try { const token = await ndAuth(); const res = await fetch(`${S.cfg.url}/api/playlist/${S._currentPlaylist.id}/image`, { method: 'DELETE', headers: {'X-Nd-Authorization': 'Bearer '+token}, }); if(!res.ok) throw new Error(res.status); toast('Cover entfernt'); S._currentPlaylist.coverArt = null; document.getElementById('edit-pl-cover').innerHTML = '
🎶
'; document.getElementById('playlist-cover').innerHTML = '
🎶
'; bustSidebarCover(S._currentPlaylist?.id, null); } catch(e) { S._ndToken = null; toast('Fehler: '+e.message); } } export async function deletePlaylist() { const pl = S._currentPlaylist; if (!pl) return; const ok = await showConfirm('Playlist löschen', `"${pl.name}" wird unwiderruflich gelöscht.`, 'Löschen'); if (!ok) return; try { await api('deletePlaylist', `&id=${pl.id}`); closeEditPlModal(); toast(`🗑 "${pl.name}" gelöscht`); invalidateApiCache('getPlaylists'); // drop the 3-min cache so the list reloads without the deleted playlist renderSidebarPlaylists(); navigate('playlists'); } catch(e) { toast('Fehler beim Löschen: ' + e.message); } }