feat: drag-and-drop reordering for playlist tracks
Drag handle appears on hover, moves items in DOM, syncs order to songs array, and saves to Navidrome via createPlaylist 800ms after last drop (debounced to avoid rapid API calls). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
ac5cbfb3de
commit
8deeaa6475
2 files changed
+73
No files matched your search
@@ -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 = `<p style="color:var(--danger)">${e.message}</p>`; }
|
||||
}
|
||||
|
||||
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 = '<svg width="10" height="14" viewBox="0 0 10 14" fill="currentColor"><circle cx="3" cy="2" r="1.3"/><circle cx="7" cy="2" r="1.3"/><circle cx="3" cy="7" r="1.3"/><circle cx="7" cy="7" r="1.3"/><circle cx="3" cy="12" r="1.3"/><circle cx="7" cy="12" r="1.3"/></svg>';
|
||||
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); }
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user