From 9249a5a4a759c7cf8433198922e94d7647977dde Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 14:55:06 +0200 Subject: [PATCH] feat(reorder): support dragging multiple selected songs to a specific position and to end (block move) --- packages/backend/src/routes/playlists.ts | 65 +++++++++++++++++++ .../src/components/PaginatedSongList.tsx | 48 +++++++++++--- packages/frontend/src/services/api.ts | 9 +++ 3 files changed, 113 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/routes/playlists.ts b/packages/backend/src/routes/playlists.ts index b1e6e40..8ced19d 100644 --- a/packages/backend/src/routes/playlists.ts +++ b/packages/backend/src/routes/playlists.ts @@ -184,4 +184,69 @@ router.post('/reorder-move', async (req: Request, res: Response) => { } }); +// Move multiple tracks before another (en bloc, preserves relative order) +router.post('/reorder-move-many', async (req: Request, res: Response) => { + try { + const { playlistName, fromIds, toId } = req.body as { playlistName: string; fromIds: string[]; toId?: string }; + if (!playlistName || !Array.isArray(fromIds) || fromIds.length === 0) { + return res.status(400).json({ message: 'playlistName and fromIds are required' }); + } + + const playlists = await Playlist.find({}); + let updated = false; + + const applyMoveMany = (node: any): any => { + if (!node) return node; + if (node.type === 'playlist' && node.name === playlistName) { + const base: string[] = Array.isArray(node.tracks) ? node.tracks : []; + const order: string[] = Array.isArray(node.order) ? node.order : []; + const baseSet = new Set(base); + const effective: string[] = [ + ...order.filter((id: string) => baseSet.has(id)), + ...base.filter((id: string) => !order.includes(id)) + ]; + + const moveSet = new Set(fromIds); + const movingOrdered = effective.filter(id => moveSet.has(id)); + if (movingOrdered.length === 0) { + return node; + } + + // Remove all moving ids + const without = effective.filter(id => !moveSet.has(id)); + let insertIndex = typeof toId === 'string' ? without.indexOf(toId) : without.length; + if (insertIndex < 0) insertIndex = without.length; + // Insert block preserving relative order + without.splice(insertIndex, 0, ...movingOrdered); + + console.log('[REORDER_MOVE_MANY] playlist:', playlistName, 'count:', movingOrdered.length, 'to:', toId, 'baseLen:', base.length, 'orderLenBefore:', order.length, 'orderLenAfter:', without.length); + console.log('[REORDER_MOVE_MANY] sample order:', without.slice(0, 7)); + node.order = without; + updated = true; + return node; + } + if (Array.isArray(node.children)) { + node.children = node.children.map(applyMoveMany); + } + return node; + }; + + for (const p of playlists) { + applyMoveMany(p as any); + p.markModified('children'); + p.markModified('order'); + await p.save(); + } + + if (!updated) { + return res.status(404).json({ message: `Playlist "${playlistName}" not found` }); + } + + res.json({ message: 'Tracks moved', playlistName }); + } catch (error) { + console.error('Error moving tracks in playlist:', error); + res.status(500).json({ message: 'Error moving tracks in playlist', error }); + } +}); + export const playlistsRouter = router; \ No newline at end of file diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 0c62a8b..59994e4 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -287,12 +287,22 @@ export const PaginatedSongList: React.FC = memo(({ onRowDrop={async (e: React.DragEvent) => { if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return; e.preventDefault(); - const fromId = e.dataTransfer.getData('text/song-id'); - if (!fromId) { + const fromId = e.dataTransfer.getData('text/song-id'); + const multiJson = e.dataTransfer.getData('application/json'); + let multiIds: string[] | null = null; + if (multiJson) { + try { + const parsed = JSON.parse(multiJson); + if (parsed && parsed.type === 'songs' && Array.isArray(parsed.songIds)) { + multiIds = parsed.songIds as string[]; + } + } catch {} + } + if (!fromId && !multiIds) { console.debug('[Reorder] missing fromId'); return; } - const fromIndex = songs.findIndex(s => s.id === fromId); + const fromIndex = fromId ? songs.findIndex(s => s.id === fromId) : -1; const toIndex = index; if (fromIndex < 0 || toIndex < 0) { console.debug('[Reorder] invalid indexes', { fromIndex, toIndex }); @@ -303,9 +313,14 @@ export const PaginatedSongList: React.FC = memo(({ return; } const toId = songs[index].id; - // Simpler and more robust: instruct backend to move fromId before toId - console.debug('[Reorder] move request', { playlist: currentPlaylist, fromId, toId }); - await api.moveTrackInPlaylist(currentPlaylist, fromId, toId); + // If multiple, move block; else move single + if (multiIds && multiIds.length > 0) { + console.debug('[Reorder] move many request', { playlist: currentPlaylist, fromIds: multiIds, toId }); + await api.moveTracksInPlaylist(currentPlaylist, multiIds, toId); + } else { + console.debug('[Reorder] move request', { playlist: currentPlaylist, fromId, toId }); + await api.moveTrackInPlaylist(currentPlaylist, fromId!, toId); + } await onReorder(songs.map(s => s.id)); // trigger refresh via parent setDragHoverIndex(null); setIsReorderDragging(false); @@ -521,10 +536,25 @@ export const PaginatedSongList: React.FC = memo(({ onDrop={async (e: React.DragEvent) => { e.preventDefault(); const fromId = e.dataTransfer.getData('text/song-id'); - if (!fromId) return; + const multiJson = e.dataTransfer.getData('application/json'); + let multiIds: string[] | null = null; + if (multiJson) { + try { + const parsed = JSON.parse(multiJson); + if (parsed && parsed.type === 'songs' && Array.isArray(parsed.songIds)) { + multiIds = parsed.songIds as string[]; + } + } catch {} + } + if (!fromId && !multiIds) return; // Move to end: omit toId - console.debug('[Reorder] move to end request', { playlist: currentPlaylist, fromId }); - await api.moveTrackInPlaylist(currentPlaylist, fromId); + if (multiIds && multiIds.length > 0) { + console.debug('[Reorder] move many to end request', { playlist: currentPlaylist, fromIds: multiIds }); + await api.moveTracksInPlaylist(currentPlaylist, multiIds); + } else { + console.debug('[Reorder] move to end request', { playlist: currentPlaylist, fromId }); + await api.moveTrackInPlaylist(currentPlaylist, fromId!); + } await onReorder(songs.map(s => s.id)); setEndDropHover(false); setIsReorderDragging(false); diff --git a/packages/frontend/src/services/api.ts b/packages/frontend/src/services/api.ts index 559467f..cd94009 100644 --- a/packages/frontend/src/services/api.ts +++ b/packages/frontend/src/services/api.ts @@ -115,6 +115,15 @@ class Api { if (!response.ok) throw new Error('Failed to move track in playlist'); } + async moveTracksInPlaylist(playlistName: string, fromIds: string[], toId?: string): Promise { + const response = await fetch(`${API_BASE_URL}/playlists/reorder-move-many`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ playlistName, fromIds, toId }) + }); + if (!response.ok) throw new Error('Failed to move tracks in playlist'); + } + async resetDatabase(): Promise { try { const response = await fetch(`${API_BASE_URL}/reset`, {