diff --git a/src/views.js b/src/views.js index 5e10ec3..250cfa4 100644 --- a/src/views.js +++ b/src/views.js @@ -542,10 +542,76 @@ export async function loadPlaylistDetail(pl) { playQueue(shuffled, 0); }; sl.innerHTML = ''; + sl.classList.add('pl-draggable'); songs.forEach((s,i) => sl.appendChild(makeSongItem(s, i, songs, { noArtistLink: true, noAlbumLink: true }))); + _initPlaylistDragDrop(sl, songs, S._currentPlaylist); } catch(e) { sl.innerHTML = `
${e.message}
`; } } +function _initPlaylistDragDrop(container, songs, pl) { + let dragSrc = null; + let saveTimer = null; + + Array.from(container.children).forEach((item, i) => { + const handle = document.createElement('div'); + handle.className = 'pl-drag-handle'; + handle.innerHTML = ''; + item.insertBefore(handle, item.firstChild); + item._songRef = songs[i]; + + item.addEventListener('dragstart', e => { + if (!e.target.closest('.pl-drag-handle')) { e.preventDefault(); return; } + dragSrc = item; + e.dataTransfer.effectAllowed = 'move'; + setTimeout(() => item.classList.add('dragging'), 0); + }); + item.addEventListener('dragend', () => { + item.classList.remove('dragging'); + container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over')); + }); + item.addEventListener('dragover', e => { + e.preventDefault(); + if (item === dragSrc) return; + container.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over')); + item.classList.add('drag-over'); + }); + item.addEventListener('dragleave', e => { + if (!item.contains(e.relatedTarget)) item.classList.remove('drag-over'); + }); + item.addEventListener('drop', e => { + e.preventDefault(); + if (!dragSrc || item === dragSrc) return; + item.classList.remove('drag-over'); + const allItems = Array.from(container.children); + const fromIdx = allItems.indexOf(dragSrc); + const toIdx = allItems.indexOf(item); + container.insertBefore(dragSrc, fromIdx < toIdx ? item.nextSibling : item); + // Sync songs array to new DOM order + const newOrder = Array.from(container.children).map(el => el._songRef); + songs.splice(0, songs.length, ...newOrder); + // Update track numbers + Array.from(container.children).forEach((el, idx) => { + const n = el.querySelector('.song-num'); + if (n) n.textContent = idx + 1; + }); + // Debounced save + clearTimeout(saveTimer); + saveTimer = setTimeout(() => _savePlaylistOrder(pl, songs), 800); + }); + + handle.addEventListener('mousedown', () => { item.draggable = true; }); + item.addEventListener('dragend', () => { item.draggable = false; }); + }); +} + +async function _savePlaylistOrder(pl, songs) { + try { + const songIds = songs.map(s => `&songId=${s.id}`).join(''); + await api('createPlaylist', `&playlistId=${pl.id}&name=${encodeURIComponent(pl.name)}${songIds}`); + toast('✅ Reihenfolge gespeichert'); + } catch(e) { toast('Fehler beim Speichern: ' + e.message); } +} + export async function quickPlayAlbum(a) { try { const r = await api('getAlbum', `&id=${a.id}`); playQueue(r.album?.song || [], 0); } catch(e) { toast('Fehler: ' + e.message); } diff --git a/styles.css b/styles.css index 9095c53..2c21c02 100644 --- a/styles.css +++ b/styles.css @@ -189,6 +189,13 @@ nav{flex:1;overflow-y:auto;padding:4px 10px} .song-dur{font-size:11px;color:var(--text3);font-family:var(--mono)} .song-actions{display:flex;gap:2px;opacity:0;transition:opacity .15s} .song-item:hover .song-actions{opacity:1} +/* ── PLAYLIST DRAG & DROP ── */ +.song-list.pl-draggable .song-item{grid-template-columns:18px 32px 44px 1fr auto} +.pl-drag-handle{display:flex;align-items:center;justify-content:center;color:var(--text3);opacity:0;cursor:grab;transition:opacity .15s;touch-action:none} +.song-list.pl-draggable .song-item:hover .pl-drag-handle{opacity:1} +.pl-drag-handle:active{cursor:grabbing} +.song-item.dragging{opacity:0.35} +.song-item.drag-over{box-shadow:0 -2px 0 var(--accent)} /* ── ALBUM DETAIL ── */ .back-btn{background:none;border:none;color:var(--text2);cursor:pointer;display:flex;align-items:center;gap:6px;font-family:var(--font);font-size:12px;padding:6px 0;margin-bottom:16px;transition:color .15s}