From 6917c22b94311eced001dfb4404cb5d00a0609c1 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 13:00:40 +0200 Subject: [PATCH 01/14] feat(dnd): drag songs (with multiselect) into sidebar playlists; drop handler adds to playlist with duplicate confirm (allow or skip) --- packages/frontend/src/App.tsx | 30 +++++++++++++++++++ .../src/components/PaginatedSongList.tsx | 20 +++++++++++-- .../src/components/PlaylistManager.tsx | 25 +++++++++++++++- 3 files changed, 72 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 4f159c9..efbd11d 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -258,6 +258,35 @@ const RekordboxReader: React.FC = () => { setPlaylists(savedPlaylists); }; + // Handle drop from song list into playlist (with duplicate check and user choice) + const handleDropSongsToPlaylist = async (playlistName: string, songIds: string[]) => { + // Find target playlist current tracks + const findNode = (nodes: PlaylistNode[]): PlaylistNode | null => { + for (const n of nodes) { + if (n.name === playlistName && n.type === 'playlist') return n; + if (n.type === 'folder' && n.children) { + const found = findNode(n.children); + if (found) return found; + } + } + return null; + }; + const target = findNode(playlists); + const existing = new Set(target?.tracks || []); + const dupes = songIds.filter(id => existing.has(id)); + + let proceedMode: 'skip' | 'allow' = 'skip'; + if (dupes.length > 0) { + // Simple confirm flow: OK = allow duplicates, Cancel = skip duplicates + const allow = window.confirm(`${dupes.length} duplicate${dupes.length>1?'s':''} detected in "${playlistName}". Press OK to add anyway (allow duplicates) or Cancel to skip duplicates.`); + proceedMode = allow ? 'allow' : 'skip'; + } + + const finalIds = proceedMode === 'skip' ? songIds.filter(id => !existing.has(id)) : songIds; + if (finalIds.length === 0) return; + await handleAddSongsToPlaylist(finalIds, playlistName); + }; + const handleRemoveFromPlaylist = async (songIds: string[]) => { if (currentPlaylist === "All Songs") return; @@ -424,6 +453,7 @@ const RekordboxReader: React.FC = () => { onPlaylistDelete={handlePlaylistDelete} onFolderCreate={handleCreateFolder} onPlaylistMove={handleMovePlaylist} + onDropSongs={handleDropSongsToPlaylist} /> ); diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 6a7a3ac..04224db 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -49,7 +49,8 @@ const SongItem = memo<{ onToggleSelection: (songId: string) => void; showCheckbox: boolean; onPlaySong?: (song: Song) => void; -}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong }) => { + onDragStart: (songIds: string[]) => void; +}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong, onDragStart }) => { // Memoize the formatted duration to prevent recalculation const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]); const handleClick = useCallback(() => { @@ -83,6 +84,13 @@ const SongItem = memo<{ _hover={{ bg: isHighlighted ? "blue.800" : "gray.700" }} onClick={handleClick} transition="background-color 0.2s" + draggable + onDragStart={(e) => { + // Mark this item as part of the drag selection using a custom type + onDragStart([song.id]); + e.dataTransfer.setData('application/json', JSON.stringify({ type: 'songs', songIds: [song.id] })); + e.dataTransfer.effectAllowed = 'copyMove'; + }} > {showCheckbox && ( = memo(({ onPlaySong }) => { const [selectedSongs, setSelectedSongs] = useState>(new Set()); + const dragSelectionRef = useRef(null); const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery); const { isOpen: isPlaylistModalOpen, onOpen: onPlaylistModalOpen, onClose: onPlaylistModalClose } = useDisclosure(); const observerRef = useRef(null); @@ -227,6 +236,12 @@ export const PaginatedSongList: React.FC = memo(({ // Memoized search handler with debouncing // Search handled inline via localSearchQuery effect + // Provide drag payload: if multiple selected, drag all; else drag the single item + const handleDragStart = useCallback((songIdsFallback: string[]) => { + const ids = selectedSongs.size > 0 ? Array.from(selectedSongs) : songIdsFallback; + dragSelectionRef.current = ids; + }, [selectedSongs]); + // Memoized song items to prevent unnecessary re-renders const songItems = useMemo(() => { return songs.map(song => ( @@ -239,9 +254,10 @@ export const PaginatedSongList: React.FC = memo(({ onToggleSelection={toggleSelection} showCheckbox={selectedSongs.size > 0 || depth === 0} onPlaySong={onPlaySong} + onDragStart={handleDragStart} /> )); - }, [songs, selectedSongs, selectedSongId, toggleSelection, depth, onPlaySong]); // Removed handleSongSelect since it's already memoized + }, [songs, selectedSongs, selectedSongId, toggleSelection, depth, onPlaySong, handleDragStart]); // Removed handleSongSelect since it's already memoized // Use total playlist duration if available, otherwise calculate from current songs const totalDuration = useMemo(() => { diff --git a/packages/frontend/src/components/PlaylistManager.tsx b/packages/frontend/src/components/PlaylistManager.tsx index 9f71b86..a0dd91c 100644 --- a/packages/frontend/src/components/PlaylistManager.tsx +++ b/packages/frontend/src/components/PlaylistManager.tsx @@ -17,7 +17,7 @@ import { MenuList, MenuItem, Text, - HStack, + // HStack, Collapse, MenuDivider, MenuGroup, @@ -36,6 +36,7 @@ interface PlaylistManagerProps { onPlaylistDelete: (name: string) => void; onFolderCreate: (name: string) => void; onPlaylistMove: (playlistName: string, targetFolderName: string | null) => void; + onDropSongs?: (playlistName: string, songIds: string[]) => void; } // Memoized button styles to prevent unnecessary re-renders @@ -91,6 +92,7 @@ interface PlaylistItemProps { onPlaylistDelete: (name: string) => void; onPlaylistMove: (playlistName: string, targetFolderName: string | null) => void; allFolders: { name: string }[]; + onDropSongs?: (playlistName: string, songIds: string[]) => void; } const PlaylistItem: React.FC = React.memo(({ @@ -101,6 +103,7 @@ const PlaylistItem: React.FC = React.memo(({ onPlaylistDelete, onPlaylistMove, allFolders, + onDropSongs, }) => { const [isOpen, setIsOpen] = useState(false); @@ -137,6 +140,16 @@ const PlaylistItem: React.FC = React.memo(({ /> } + onDragOver={(e) => e.preventDefault()} + onDrop={(e) => { + try { + const data = e.dataTransfer.getData('application/json'); + const parsed = JSON.parse(data); + if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) { + onDropSongs(node.name, parsed.songIds); + } + } catch {} + }} > {level > 0 && ( = React.memo(({ borderRight="1px solid" borderRightColor="whiteAlpha.200" position="relative" + onDragOver={(e) => e.preventDefault()} + onDrop={(e) => { + try { + const data = e.dataTransfer.getData('application/json'); + const parsed = JSON.parse(data); + if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) { + onDropSongs(node.name, parsed.songIds); + } + } catch {} + }} > {level > 0 && ( Date: Fri, 8 Aug 2025 13:08:22 +0200 Subject: [PATCH 02/14] feat(dnd): highlight playlist drop target; show drag count badge; refine drag payload and lifecycle --- .../src/components/PaginatedSongList.tsx | 27 ++++++++++++++----- .../src/components/PlaylistManager.tsx | 16 +++++++++-- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 04224db..7314eb3 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -49,7 +49,7 @@ const SongItem = memo<{ onToggleSelection: (songId: string) => void; showCheckbox: boolean; onPlaySong?: (song: Song) => void; - onDragStart: (songIds: string[]) => void; + onDragStart: (e: React.DragEvent, songIdsFallback: string[]) => void; }>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong, onDragStart }) => { // Memoize the formatted duration to prevent recalculation const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]); @@ -86,10 +86,7 @@ const SongItem = memo<{ transition="background-color 0.2s" draggable onDragStart={(e) => { - // Mark this item as part of the drag selection using a custom type - onDragStart([song.id]); - e.dataTransfer.setData('application/json', JSON.stringify({ type: 'songs', songIds: [song.id] })); - e.dataTransfer.effectAllowed = 'copyMove'; + onDragStart(e, [song.id]); }} > {showCheckbox && ( @@ -155,6 +152,7 @@ export const PaginatedSongList: React.FC = memo(({ }) => { const [selectedSongs, setSelectedSongs] = useState>(new Set()); const dragSelectionRef = useRef(null); + const [isDragging, setIsDragging] = useState(false); const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery); const { isOpen: isPlaylistModalOpen, onOpen: onPlaylistModalOpen, onClose: onPlaylistModalClose } = useDisclosure(); const observerRef = useRef(null); @@ -237,11 +235,20 @@ export const PaginatedSongList: React.FC = memo(({ // Search handled inline via localSearchQuery effect // Provide drag payload: if multiple selected, drag all; else drag the single item - const handleDragStart = useCallback((songIdsFallback: string[]) => { + const handleDragStart = useCallback((e: React.DragEvent, songIdsFallback: string[]) => { const ids = selectedSongs.size > 0 ? Array.from(selectedSongs) : songIdsFallback; dragSelectionRef.current = ids; + setIsDragging(true); + const payload = { type: 'songs', songIds: ids, count: ids.length }; + e.dataTransfer.setData('application/json', JSON.stringify(payload)); + e.dataTransfer.effectAllowed = 'copyMove'; }, [selectedSongs]); + const handleDragEnd = useCallback(() => { + setIsDragging(false); + dragSelectionRef.current = null; + }, []); + // Memoized song items to prevent unnecessary re-renders const songItems = useMemo(() => { return songs.map(song => ( @@ -325,6 +332,12 @@ export const PaginatedSongList: React.FC = memo(({ return ( + {/* Global drag badge for selected count */} + {isDragging && ( + + Dragging {dragSelectionRef.current?.length || 0} song{(dragSelectionRef.current?.length || 0) === 1 ? '' : 's'} + + )} {/* Sticky Header */} = memo(({ id="song-list-container" > - {songItems} + {songItems} {/* Loading indicator for infinite scroll or playlist switching */} {(loading || isSwitchingPlaylist) && ( diff --git a/packages/frontend/src/components/PlaylistManager.tsx b/packages/frontend/src/components/PlaylistManager.tsx index a0dd91c..06b2138 100644 --- a/packages/frontend/src/components/PlaylistManager.tsx +++ b/packages/frontend/src/components/PlaylistManager.tsx @@ -106,6 +106,7 @@ const PlaylistItem: React.FC = React.memo(({ onDropSongs, }) => { const [isOpen, setIsOpen] = useState(false); + const [isDragOver, setIsDragOver] = useState(false); // Memoize click handlers to prevent recreation const handlePlaylistClick = useCallback(() => { @@ -140,7 +141,11 @@ const PlaylistItem: React.FC = React.memo(({ /> } - onDragOver={(e) => e.preventDefault()} + onDragOver={(e) => { + e.preventDefault(); + setIsDragOver(true); + }} + onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { try { const data = e.dataTransfer.getData('application/json'); @@ -149,7 +154,10 @@ const PlaylistItem: React.FC = React.memo(({ onDropSongs(node.name, parsed.songIds); } } catch {} + setIsDragOver(false); }} + borderColor={isDragOver ? 'blue.400' : undefined} + borderWidth={isDragOver ? '1px' : undefined} > {level > 0 && ( = React.memo(({ borderRight="1px solid" borderRightColor="whiteAlpha.200" position="relative" - onDragOver={(e) => e.preventDefault()} + onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }} + onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { try { const data = e.dataTransfer.getData('application/json'); @@ -218,7 +227,10 @@ const PlaylistItem: React.FC = React.memo(({ onDropSongs(node.name, parsed.songIds); } } catch {} + setIsDragOver(false); }} + borderColor={isDragOver ? 'blue.400' : undefined} + borderWidth={isDragOver ? '1px' : undefined} > {level > 0 && ( Date: Fri, 8 Aug 2025 13:11:40 +0200 Subject: [PATCH 03/14] fix(dnd): ensure drop detects payload; set text/plain fallback; refresh playlist structure after add --- packages/frontend/src/App.tsx | 6 ++++++ .../src/components/PaginatedSongList.tsx | 1 + .../frontend/src/components/PlaylistManager.tsx | 16 ++++++++++++++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index efbd11d..f41c420 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -272,6 +272,7 @@ const RekordboxReader: React.FC = () => { return null; }; const target = findNode(playlists); + if (!target) return; const existing = new Set(target?.tracks || []); const dupes = songIds.filter(id => existing.has(id)); @@ -285,6 +286,11 @@ const RekordboxReader: React.FC = () => { const finalIds = proceedMode === 'skip' ? songIds.filter(id => !existing.has(id)) : songIds; if (finalIds.length === 0) return; await handleAddSongsToPlaylist(finalIds, playlistName); + // If we were on that playlist, refresh counters by reloading structure + try { + const updated = await api.getPlaylistStructure(); + setPlaylists(updated); + } catch {} }; const handleRemoveFromPlaylist = async (songIds: string[]) => { diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 7314eb3..e610cde 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -241,6 +241,7 @@ export const PaginatedSongList: React.FC = memo(({ setIsDragging(true); const payload = { type: 'songs', songIds: ids, count: ids.length }; e.dataTransfer.setData('application/json', JSON.stringify(payload)); + e.dataTransfer.setData('text/plain', JSON.stringify(payload)); e.dataTransfer.effectAllowed = 'copyMove'; }, [selectedSongs]); diff --git a/packages/frontend/src/components/PlaylistManager.tsx b/packages/frontend/src/components/PlaylistManager.tsx index 06b2138..a295e01 100644 --- a/packages/frontend/src/components/PlaylistManager.tsx +++ b/packages/frontend/src/components/PlaylistManager.tsx @@ -143,7 +143,12 @@ const PlaylistItem: React.FC = React.memo(({ } onDragOver={(e) => { e.preventDefault(); - setIsDragOver(true); + // Only highlight if payload looks like songs + const json = e.dataTransfer.getData('application/json') || e.dataTransfer.getData('text/plain'); + try { + const parsed = JSON.parse(json); + if (parsed?.type === 'songs') setIsDragOver(true); + } catch {} }} onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { @@ -217,7 +222,14 @@ const PlaylistItem: React.FC = React.memo(({ borderRight="1px solid" borderRightColor="whiteAlpha.200" position="relative" - onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }} + onDragOver={(e) => { + e.preventDefault(); + const json = e.dataTransfer.getData('application/json') || e.dataTransfer.getData('text/plain'); + try { + const parsed = JSON.parse(json); + if (parsed?.type === 'songs') setIsDragOver(true); + } catch {} + }} onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { try { From 8a9f51a0c6709697b5d8ce4ca776f9e7b5eac938 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 13:17:15 +0200 Subject: [PATCH 04/14] fix(dnd): align drop flow with modal add; use same backend save and show success toast; improve payload parsing for drop targets --- packages/frontend/src/App.tsx | 21 ++++++++++++------- .../src/components/PlaylistManager.tsx | 8 +++---- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index f41c420..194b27c 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Flex, Heading, Spinner, Text, useBreakpointValue, IconButton, Drawer, DrawerBody, DrawerHeader, DrawerOverlay, DrawerContent, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, VStack } from "@chakra-ui/react"; +import { Box, Button, Flex, Heading, Spinner, Text, useBreakpointValue, IconButton, Drawer, DrawerBody, DrawerHeader, DrawerOverlay, DrawerContent, useDisclosure, Modal, ModalOverlay, ModalContent, ModalHeader, ModalBody, ModalFooter, VStack, useToast } from "@chakra-ui/react"; import { ChevronLeftIcon, ChevronRightIcon, SettingsIcon, DownloadIcon } from "@chakra-ui/icons"; import React, { useState, useRef, useEffect, useCallback } from "react"; import { useNavigate, useLocation, Routes, Route } from "react-router-dom"; @@ -76,6 +76,7 @@ const RekordboxReader: React.FC = () => { const [isDatabaseInitialized, setIsDatabaseInitialized] = useState(false); const [isSwitchingPlaylist, setIsSwitchingPlaylist] = useState(false); const { currentSong, playSong, closePlayer } = useMusicPlayer(); + const toast = useToast(); // Memoized song selection handler to prevent unnecessary re-renders const handleSongSelect = useCallback((song: Song) => { @@ -254,8 +255,10 @@ const RekordboxReader: React.FC = () => { } return node; }); - const savedPlaylists = await api.savePlaylists(updatedPlaylists); - setPlaylists(savedPlaylists); + await api.savePlaylists(updatedPlaylists); + // Always normalize state to structure for consistent counters + const structure = await api.getPlaylistStructure(); + setPlaylists(structure); }; // Handle drop from song list into playlist (with duplicate check and user choice) @@ -286,11 +289,13 @@ const RekordboxReader: React.FC = () => { const finalIds = proceedMode === 'skip' ? songIds.filter(id => !existing.has(id)) : songIds; if (finalIds.length === 0) return; await handleAddSongsToPlaylist(finalIds, playlistName); - // If we were on that playlist, refresh counters by reloading structure - try { - const updated = await api.getPlaylistStructure(); - setPlaylists(updated); - } catch {} + toast({ + title: 'Songs Added', + description: `${finalIds.length} song${finalIds.length === 1 ? '' : 's'} added to "${playlistName}"`, + status: 'success', + duration: 3000, + isClosable: true, + }); }; const handleRemoveFromPlaylist = async (songIds: string[]) => { diff --git a/packages/frontend/src/components/PlaylistManager.tsx b/packages/frontend/src/components/PlaylistManager.tsx index a295e01..4fb9378 100644 --- a/packages/frontend/src/components/PlaylistManager.tsx +++ b/packages/frontend/src/components/PlaylistManager.tsx @@ -153,8 +153,8 @@ const PlaylistItem: React.FC = React.memo(({ onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { try { - const data = e.dataTransfer.getData('application/json'); - const parsed = JSON.parse(data); + const json = e.dataTransfer.getData('application/json') || e.dataTransfer.getData('text/plain'); + const parsed = JSON.parse(json); if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) { onDropSongs(node.name, parsed.songIds); } @@ -233,8 +233,8 @@ const PlaylistItem: React.FC = React.memo(({ onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { try { - const data = e.dataTransfer.getData('application/json'); - const parsed = JSON.parse(data); + const json = e.dataTransfer.getData('application/json') || e.dataTransfer.getData('text/plain'); + const parsed = JSON.parse(json); if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) { onDropSongs(node.name, parsed.songIds); } From b534485bde78116dd0d5fa54fd9306aa5759ad1d Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 13:22:20 +0200 Subject: [PATCH 05/14] chore(dnd-debug): add console.debug logs for dragover/drop, fix highlighting conditions, log drag payload --- .../src/components/PaginatedSongList.tsx | 3 ++ .../src/components/PlaylistManager.tsx | 32 ++++++++++++++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index e610cde..b2c0023 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -243,6 +243,9 @@ export const PaginatedSongList: React.FC = memo(({ e.dataTransfer.setData('application/json', JSON.stringify(payload)); e.dataTransfer.setData('text/plain', JSON.stringify(payload)); e.dataTransfer.effectAllowed = 'copyMove'; + try { + console.debug('[DnD] dragstart payload', payload); + } catch {} }, [selectedSongs]); const handleDragEnd = useCallback(() => { diff --git a/packages/frontend/src/components/PlaylistManager.tsx b/packages/frontend/src/components/PlaylistManager.tsx index 4fb9378..08e8ba7 100644 --- a/packages/frontend/src/components/PlaylistManager.tsx +++ b/packages/frontend/src/components/PlaylistManager.tsx @@ -147,8 +147,13 @@ const PlaylistItem: React.FC = React.memo(({ const json = e.dataTransfer.getData('application/json') || e.dataTransfer.getData('text/plain'); try { const parsed = JSON.parse(json); - if (parsed?.type === 'songs') setIsDragOver(true); - } catch {} + if (parsed?.type === 'songs') { + setIsDragOver(true); + } + console.debug('[DnD] dragover folder', node.name, parsed); + } catch (err) { + console.debug('[DnD] dragover folder parse failed', node.name, err); + } }} onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { @@ -158,7 +163,10 @@ const PlaylistItem: React.FC = React.memo(({ if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) { onDropSongs(node.name, parsed.songIds); } - } catch {} + console.debug('[DnD] drop folder', node.name, parsed); + } catch (err) { + console.debug('[DnD] drop folder parse failed', node.name, err); + } setIsDragOver(false); }} borderColor={isDragOver ? 'blue.400' : undefined} @@ -227,8 +235,13 @@ const PlaylistItem: React.FC = React.memo(({ const json = e.dataTransfer.getData('application/json') || e.dataTransfer.getData('text/plain'); try { const parsed = JSON.parse(json); - if (parsed?.type === 'songs') setIsDragOver(true); - } catch {} + if (parsed?.type === 'songs') { + setIsDragOver(true); + } + console.debug('[DnD] dragover playlist', node.name, parsed); + } catch (err) { + console.debug('[DnD] dragover playlist parse failed', node.name, err); + } }} onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { @@ -238,11 +251,14 @@ const PlaylistItem: React.FC = React.memo(({ if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) { onDropSongs(node.name, parsed.songIds); } - } catch {} + console.debug('[DnD] drop playlist', node.name, parsed); + } catch (err) { + console.debug('[DnD] drop playlist parse failed', node.name, err); + } setIsDragOver(false); }} - borderColor={isDragOver ? 'blue.400' : undefined} - borderWidth={isDragOver ? '1px' : undefined} + borderColor={isDragOver ? 'blue.400' : (selectedItem === node.name ? 'blue.700' : 'transparent')} + borderWidth={isDragOver || selectedItem === node.name ? '1px' : undefined} > {level > 0 && ( Date: Fri, 8 Aug 2025 13:24:13 +0200 Subject: [PATCH 06/14] fix(dnd): avoid parsing during dragover; only inspect types; parse on drop with guards and log raw length --- .../src/components/PlaylistManager.tsx | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/packages/frontend/src/components/PlaylistManager.tsx b/packages/frontend/src/components/PlaylistManager.tsx index 08e8ba7..9a79d32 100644 --- a/packages/frontend/src/components/PlaylistManager.tsx +++ b/packages/frontend/src/components/PlaylistManager.tsx @@ -143,27 +143,22 @@ const PlaylistItem: React.FC = React.memo(({ } onDragOver={(e) => { e.preventDefault(); - // Only highlight if payload looks like songs - const json = e.dataTransfer.getData('application/json') || e.dataTransfer.getData('text/plain'); - try { - const parsed = JSON.parse(json); - if (parsed?.type === 'songs') { - setIsDragOver(true); - } - console.debug('[DnD] dragover folder', node.name, parsed); - } catch (err) { - console.debug('[DnD] dragover folder parse failed', node.name, err); + const types = Array.from((e.dataTransfer.types || []) as any); + const hasOurType = types.includes('application/json') || types.includes('text/plain'); + if (hasOurType) { + setIsDragOver(true); } + console.debug('[DnD] dragover folder types', node.name, types); }} onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { try { const json = e.dataTransfer.getData('application/json') || e.dataTransfer.getData('text/plain'); - const parsed = JSON.parse(json); + const parsed = json ? JSON.parse(json) : null; if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) { onDropSongs(node.name, parsed.songIds); } - console.debug('[DnD] drop folder', node.name, parsed); + console.debug('[DnD] drop folder', node.name, parsed, 'raw len', json?.length || 0); } catch (err) { console.debug('[DnD] drop folder parse failed', node.name, err); } @@ -232,26 +227,22 @@ const PlaylistItem: React.FC = React.memo(({ position="relative" onDragOver={(e) => { e.preventDefault(); - const json = e.dataTransfer.getData('application/json') || e.dataTransfer.getData('text/plain'); - try { - const parsed = JSON.parse(json); - if (parsed?.type === 'songs') { - setIsDragOver(true); - } - console.debug('[DnD] dragover playlist', node.name, parsed); - } catch (err) { - console.debug('[DnD] dragover playlist parse failed', node.name, err); + const types = Array.from((e.dataTransfer.types || []) as any); + const hasOurType = types.includes('application/json') || types.includes('text/plain'); + if (hasOurType) { + setIsDragOver(true); } + console.debug('[DnD] dragover playlist types', node.name, types); }} onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { try { const json = e.dataTransfer.getData('application/json') || e.dataTransfer.getData('text/plain'); - const parsed = JSON.parse(json); + const parsed = json ? JSON.parse(json) : null; if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) { onDropSongs(node.name, parsed.songIds); } - console.debug('[DnD] drop playlist', node.name, parsed); + console.debug('[DnD] drop playlist', node.name, parsed, 'raw len', json?.length || 0); } catch (err) { console.debug('[DnD] drop playlist parse failed', node.name, err); } From 8394f4b42f2e62ecb4d670f7598c073d4e2bf96b Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 13:25:16 +0200 Subject: [PATCH 07/14] ui(playlists): restore subtle borders; use box-shadow for drag highlight without altering base border styles --- packages/frontend/src/components/PlaylistManager.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/components/PlaylistManager.tsx b/packages/frontend/src/components/PlaylistManager.tsx index 9a79d32..859c3bb 100644 --- a/packages/frontend/src/components/PlaylistManager.tsx +++ b/packages/frontend/src/components/PlaylistManager.tsx @@ -164,8 +164,7 @@ const PlaylistItem: React.FC = React.memo(({ } setIsDragOver(false); }} - borderColor={isDragOver ? 'blue.400' : undefined} - borderWidth={isDragOver ? '1px' : undefined} + boxShadow={isDragOver ? 'inset 0 0 0 1px var(--chakra-colors-blue-400)' : 'none'} > {level > 0 && ( = React.memo(({ borderRight="1px solid" borderRightColor="whiteAlpha.200" position="relative" + border="1px solid" + borderColor={selectedItem === node.name ? 'blue.800' : 'transparent'} + _hover={{ ...(selectedItem === node.name ? {} : { borderColor: 'whiteAlpha.300' }) }} onDragOver={(e) => { e.preventDefault(); const types = Array.from((e.dataTransfer.types || []) as any); @@ -248,8 +250,7 @@ const PlaylistItem: React.FC = React.memo(({ } setIsDragOver(false); }} - borderColor={isDragOver ? 'blue.400' : (selectedItem === node.name ? 'blue.700' : 'transparent')} - borderWidth={isDragOver || selectedItem === node.name ? '1px' : undefined} + boxShadow={isDragOver ? 'inset 0 0 0 1px var(--chakra-colors-blue-400)' : 'none'} > {level > 0 && ( Date: Fri, 8 Aug 2025 13:27:46 +0200 Subject: [PATCH 08/14] fix(dnd): robust drop parsing with window fallback; keep previous sidebar border look; add global payload for reliability --- .../frontend/src/components/PaginatedSongList.tsx | 4 ++++ .../frontend/src/components/PlaylistManager.tsx | 15 ++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index b2c0023..d64f190 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -243,6 +243,9 @@ export const PaginatedSongList: React.FC = memo(({ e.dataTransfer.setData('application/json', JSON.stringify(payload)); e.dataTransfer.setData('text/plain', JSON.stringify(payload)); e.dataTransfer.effectAllowed = 'copyMove'; + try { + (window as any).__rbDragPayload = payload; + } catch {} try { console.debug('[DnD] dragstart payload', payload); } catch {} @@ -251,6 +254,7 @@ export const PaginatedSongList: React.FC = memo(({ const handleDragEnd = useCallback(() => { setIsDragging(false); dragSelectionRef.current = null; + try { (window as any).__rbDragPayload = null; } catch {} }, []); // Memoized song items to prevent unnecessary re-renders diff --git a/packages/frontend/src/components/PlaylistManager.tsx b/packages/frontend/src/components/PlaylistManager.tsx index 859c3bb..3088291 100644 --- a/packages/frontend/src/components/PlaylistManager.tsx +++ b/packages/frontend/src/components/PlaylistManager.tsx @@ -224,9 +224,8 @@ const PlaylistItem: React.FC = React.memo(({ borderRight="1px solid" borderRightColor="whiteAlpha.200" position="relative" - border="1px solid" - borderColor={selectedItem === node.name ? 'blue.800' : 'transparent'} - _hover={{ ...(selectedItem === node.name ? {} : { borderColor: 'whiteAlpha.300' }) }} + border={selectedItem === node.name ? '1px solid' : undefined} + borderColor={selectedItem === node.name ? 'blue.800' : undefined} onDragOver={(e) => { e.preventDefault(); const types = Array.from((e.dataTransfer.types || []) as any); @@ -239,8 +238,14 @@ const PlaylistItem: React.FC = React.memo(({ onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { try { - const json = e.dataTransfer.getData('application/json') || e.dataTransfer.getData('text/plain'); - const parsed = json ? JSON.parse(json) : null; + let json = e.dataTransfer.getData('application/json'); + if (!json) json = e.dataTransfer.getData('text/plain'); + let parsed: any = null; + if (json && json.trim().length > 0) { + parsed = JSON.parse(json); + } else if ((window as any).__rbDragPayload) { + parsed = (window as any).__rbDragPayload; + } if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) { onDropSongs(node.name, parsed.songIds); } From 0c8e00389b13464c908eed9f4ce9e637aa3eae2b Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 13:29:58 +0200 Subject: [PATCH 09/14] fix(playlists): modify full playlist tree when adding tracks (avoid structure-only overwrite); then reload structure for counters --- packages/frontend/src/App.tsx | 46 ++++++++++++++++------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 194b27c..3b54475 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -232,31 +232,27 @@ const RekordboxReader: React.FC = () => { }; const handleAddSongsToPlaylist = async (songIds: string[], playlistName: string) => { - const updatedPlaylists = playlists.map(node => { - if (node.name === playlistName && node.type === 'playlist') { - return { - ...node, - tracks: [...new Set([...(node.tracks || []), ...songIds])] - }; - } - if (node.type === 'folder' && node.children) { - return { - ...node, - children: node.children.map(child => { - if (child.name === playlistName && child.type === 'playlist') { - return { - ...child, - tracks: [...new Set([...(child.tracks || []), ...songIds])] - }; - } - return child; - }) - }; - } - return node; - }); - await api.savePlaylists(updatedPlaylists); - // Always normalize state to structure for consistent counters + // Fetch FULL playlists to avoid losing tracks (structure view strips them) + const fullTree = await api.getPlaylists(); + + const applyAdd = (nodes: PlaylistNode[]): PlaylistNode[] => { + return nodes.map(node => { + if (node.type === 'playlist' && node.name === playlistName) { + const current = Array.isArray(node.tracks) ? node.tracks : []; + const merged = Array.from(new Set([...current, ...songIds])); + return { ...node, tracks: merged }; + } + if (node.type === 'folder' && node.children) { + return { ...node, children: applyAdd(node.children) }; + } + return node; + }); + }; + + const updatedFullTree = applyAdd(fullTree); + await api.savePlaylists(updatedFullTree); + + // Reload structure for UI counters const structure = await api.getPlaylistStructure(); setPlaylists(structure); }; From 3f57904dd7989fe92b3a7abad9e87453b259894f Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 13:35:54 +0200 Subject: [PATCH 10/14] fix(dnd): ensure drop triggers by preventing default and stopping propagation; set dropEffect=copy on dragover --- packages/frontend/src/components/PlaylistManager.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/frontend/src/components/PlaylistManager.tsx b/packages/frontend/src/components/PlaylistManager.tsx index 3088291..cd7d0f6 100644 --- a/packages/frontend/src/components/PlaylistManager.tsx +++ b/packages/frontend/src/components/PlaylistManager.tsx @@ -143,15 +143,19 @@ const PlaylistItem: React.FC = React.memo(({ } onDragOver={(e) => { e.preventDefault(); + e.stopPropagation(); const types = Array.from((e.dataTransfer.types || []) as any); const hasOurType = types.includes('application/json') || types.includes('text/plain'); if (hasOurType) { + try { e.dataTransfer.dropEffect = 'copy'; } catch {} setIsDragOver(true); } console.debug('[DnD] dragover folder types', node.name, types); }} onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { + e.preventDefault(); + e.stopPropagation(); try { const json = e.dataTransfer.getData('application/json') || e.dataTransfer.getData('text/plain'); const parsed = json ? JSON.parse(json) : null; @@ -228,15 +232,19 @@ const PlaylistItem: React.FC = React.memo(({ borderColor={selectedItem === node.name ? 'blue.800' : undefined} onDragOver={(e) => { e.preventDefault(); + e.stopPropagation(); const types = Array.from((e.dataTransfer.types || []) as any); const hasOurType = types.includes('application/json') || types.includes('text/plain'); if (hasOurType) { + try { e.dataTransfer.dropEffect = 'copy'; } catch {} setIsDragOver(true); } console.debug('[DnD] dragover playlist types', node.name, types); }} onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { + e.preventDefault(); + e.stopPropagation(); try { let json = e.dataTransfer.getData('application/json'); if (!json) json = e.dataTransfer.getData('text/plain'); From 6eabfdedd0b4cdbba8b65d02bbe8491a1aa8fb4d Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 13:36:50 +0200 Subject: [PATCH 11/14] perf(jobs): adaptive polling (2s with active jobs, 10s when idle) --- .../src/components/BackgroundJobProgress.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/BackgroundJobProgress.tsx b/packages/frontend/src/components/BackgroundJobProgress.tsx index 0d08576..810fe6f 100644 --- a/packages/frontend/src/components/BackgroundJobProgress.tsx +++ b/packages/frontend/src/components/BackgroundJobProgress.tsx @@ -98,7 +98,7 @@ export const BackgroundJobProgress: React.FC = ({ clearInterval(intervalRef.current); } - intervalRef.current = setInterval(async () => { + const tick = async () => { try { // Always reload job list to detect newly started jobs const jobsData = await api.getAllJobs(); @@ -116,13 +116,23 @@ export const BackgroundJobProgress: React.FC = ({ } catch (err) { // ignore transient polling errors } - }, 2000); + }; + + // Adaptive interval: 2s if active jobs, else 10s + const schedule = async () => { + await tick(); + const hasActive = (jobs || []).some(j => j.status === 'running'); + const delay = hasActive ? 2000 : 10000; + intervalRef.current = setTimeout(schedule, delay) as any; + }; + + schedule(); }; // Stop polling const stopPolling = () => { if (intervalRef.current) { - clearInterval(intervalRef.current); + clearTimeout(intervalRef.current as any); intervalRef.current = null; } }; From dc11487a9f71da711c6f000f4ccf2325480f5149 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 13:39:39 +0200 Subject: [PATCH 12/14] fix(dnd): bubble drop handler through nested folders; add debug log on handler invoke; harden /playlists/batch insertMany --- packages/backend/src/routes/playlists.ts | 6 ++++-- packages/frontend/src/App.tsx | 2 ++ packages/frontend/src/components/PlaylistManager.tsx | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/routes/playlists.ts b/packages/backend/src/routes/playlists.ts index 8b24f85..97f127d 100644 --- a/packages/backend/src/routes/playlists.ts +++ b/packages/backend/src/routes/playlists.ts @@ -69,8 +69,10 @@ router.get('/structure', async (req: Request, res: Response) => { // Save playlists in batch (replacing all existing ones) router.post('/batch', async (req, res) => { try { - await Playlist.deleteMany({}); // Clear existing playlists - const playlists = await Playlist.create(req.body); + // Replace all playlists atomically + await Playlist.deleteMany({}); + const payload = Array.isArray(req.body) ? req.body : []; + const playlists = await Playlist.insertMany(payload, { ordered: false }); res.json(playlists); } catch (error) { console.error('Error saving playlists:', error); diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 3b54475..fec6507 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -250,7 +250,9 @@ const RekordboxReader: React.FC = () => { }; const updatedFullTree = applyAdd(fullTree); + console.debug('[DnD] saving playlists with added tracks', { playlistName, addCount: songIds.length }); await api.savePlaylists(updatedFullTree); + console.debug('[DnD] playlists saved'); // Reload structure for UI counters const structure = await api.getPlaylistStructure(); diff --git a/packages/frontend/src/components/PlaylistManager.tsx b/packages/frontend/src/components/PlaylistManager.tsx index cd7d0f6..a095696 100644 --- a/packages/frontend/src/components/PlaylistManager.tsx +++ b/packages/frontend/src/components/PlaylistManager.tsx @@ -208,6 +208,7 @@ const PlaylistItem: React.FC = React.memo(({ onPlaylistDelete={onPlaylistDelete} onPlaylistMove={onPlaylistMove} allFolders={allFolders} + onDropSongs={onDropSongs} /> ))} @@ -255,6 +256,7 @@ const PlaylistItem: React.FC = React.memo(({ parsed = (window as any).__rbDragPayload; } if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) { + console.debug('[DnD] invoking onDropSongs', node.name, parsed.songIds.length); onDropSongs(node.name, parsed.songIds); } console.debug('[DnD] drop playlist', node.name, parsed, 'raw len', json?.length || 0); From e622219e12e25e8da122b6eb14363ae8b99de992 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 13:48:23 +0200 Subject: [PATCH 13/14] fix(dnd): pass onDropSongs into top-level PlaylistItem instances as well (not only nested) --- packages/frontend/src/App.tsx | 12 ++++++++++-- packages/frontend/src/components/PlaylistManager.tsx | 9 +++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index fec6507..e7c91f0 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -261,6 +261,7 @@ const RekordboxReader: React.FC = () => { // Handle drop from song list into playlist (with duplicate check and user choice) const handleDropSongsToPlaylist = async (playlistName: string, songIds: string[]) => { + console.debug('[DnD] App received drop', { playlistName, count: songIds?.length, songIds }); // Find target playlist current tracks const findNode = (nodes: PlaylistNode[]): PlaylistNode | null => { for (const n of nodes) { @@ -273,7 +274,10 @@ const RekordboxReader: React.FC = () => { return null; }; const target = findNode(playlists); - if (!target) return; + if (!target) { + console.debug('[DnD] target playlist not found', playlistName); + return; + } const existing = new Set(target?.tracks || []); const dupes = songIds.filter(id => existing.has(id)); @@ -285,8 +289,12 @@ const RekordboxReader: React.FC = () => { } const finalIds = proceedMode === 'skip' ? songIds.filter(id => !existing.has(id)) : songIds; - if (finalIds.length === 0) return; + if (finalIds.length === 0) { + console.debug('[DnD] nothing to add after duplicate filter'); + return; + } await handleAddSongsToPlaylist(finalIds, playlistName); + console.debug('[DnD] add completed'); toast({ title: 'Songs Added', description: `${finalIds.length} song${finalIds.length === 1 ? '' : 's'} added to "${playlistName}"`, diff --git a/packages/frontend/src/components/PlaylistManager.tsx b/packages/frontend/src/components/PlaylistManager.tsx index a095696..fab6554 100644 --- a/packages/frontend/src/components/PlaylistManager.tsx +++ b/packages/frontend/src/components/PlaylistManager.tsx @@ -255,9 +255,17 @@ const PlaylistItem: React.FC = React.memo(({ } else if ((window as any).__rbDragPayload) { parsed = (window as any).__rbDragPayload; } + console.debug('[DnD] drop playlist handler presence', node.name, Boolean(onDropSongs)); if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) { console.debug('[DnD] invoking onDropSongs', node.name, parsed.songIds.length); onDropSongs(node.name, parsed.songIds); + } else { + console.debug('[DnD] drop playlist no action', node.name, { + hasParsed: Boolean(parsed), + type: parsed?.type, + isArray: Array.isArray(parsed?.songIds), + hasHandler: Boolean(onDropSongs) + }); } console.debug('[DnD] drop playlist', node.name, parsed, 'raw len', json?.length || 0); } catch (err) { @@ -448,6 +456,7 @@ export const PlaylistManager: React.FC = ({ onPlaylistDelete={onPlaylistDelete} onPlaylistMove={onPlaylistMove} allFolders={allFolders} + onDropSongs={onDropSongs} /> ))} From 91fd5077d4646f8defcf9f718d68dfdb34442eb0 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Fri, 8 Aug 2025 13:52:32 +0200 Subject: [PATCH 14/14] chore(dnd): remove debug logs and globals; keep clean drag/drop with proper handlers and UX; no functional changes --- packages/frontend/src/App.tsx | 14 ++--------- .../src/components/PaginatedSongList.tsx | 7 ------ .../src/components/PlaylistManager.tsx | 24 +++---------------- 3 files changed, 5 insertions(+), 40 deletions(-) diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index e7c91f0..3b54475 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -250,9 +250,7 @@ const RekordboxReader: React.FC = () => { }; const updatedFullTree = applyAdd(fullTree); - console.debug('[DnD] saving playlists with added tracks', { playlistName, addCount: songIds.length }); await api.savePlaylists(updatedFullTree); - console.debug('[DnD] playlists saved'); // Reload structure for UI counters const structure = await api.getPlaylistStructure(); @@ -261,7 +259,6 @@ const RekordboxReader: React.FC = () => { // Handle drop from song list into playlist (with duplicate check and user choice) const handleDropSongsToPlaylist = async (playlistName: string, songIds: string[]) => { - console.debug('[DnD] App received drop', { playlistName, count: songIds?.length, songIds }); // Find target playlist current tracks const findNode = (nodes: PlaylistNode[]): PlaylistNode | null => { for (const n of nodes) { @@ -274,10 +271,7 @@ const RekordboxReader: React.FC = () => { return null; }; const target = findNode(playlists); - if (!target) { - console.debug('[DnD] target playlist not found', playlistName); - return; - } + if (!target) return; const existing = new Set(target?.tracks || []); const dupes = songIds.filter(id => existing.has(id)); @@ -289,12 +283,8 @@ const RekordboxReader: React.FC = () => { } const finalIds = proceedMode === 'skip' ? songIds.filter(id => !existing.has(id)) : songIds; - if (finalIds.length === 0) { - console.debug('[DnD] nothing to add after duplicate filter'); - return; - } + if (finalIds.length === 0) return; await handleAddSongsToPlaylist(finalIds, playlistName); - console.debug('[DnD] add completed'); toast({ title: 'Songs Added', description: `${finalIds.length} song${finalIds.length === 1 ? '' : 's'} added to "${playlistName}"`, diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index d64f190..e610cde 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -243,18 +243,11 @@ export const PaginatedSongList: React.FC = memo(({ e.dataTransfer.setData('application/json', JSON.stringify(payload)); e.dataTransfer.setData('text/plain', JSON.stringify(payload)); e.dataTransfer.effectAllowed = 'copyMove'; - try { - (window as any).__rbDragPayload = payload; - } catch {} - try { - console.debug('[DnD] dragstart payload', payload); - } catch {} }, [selectedSongs]); const handleDragEnd = useCallback(() => { setIsDragging(false); dragSelectionRef.current = null; - try { (window as any).__rbDragPayload = null; } catch {} }, []); // Memoized song items to prevent unnecessary re-renders diff --git a/packages/frontend/src/components/PlaylistManager.tsx b/packages/frontend/src/components/PlaylistManager.tsx index fab6554..9cf9826 100644 --- a/packages/frontend/src/components/PlaylistManager.tsx +++ b/packages/frontend/src/components/PlaylistManager.tsx @@ -150,7 +150,6 @@ const PlaylistItem: React.FC = React.memo(({ try { e.dataTransfer.dropEffect = 'copy'; } catch {} setIsDragOver(true); } - console.debug('[DnD] dragover folder types', node.name, types); }} onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { @@ -162,10 +161,7 @@ const PlaylistItem: React.FC = React.memo(({ if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) { onDropSongs(node.name, parsed.songIds); } - console.debug('[DnD] drop folder', node.name, parsed, 'raw len', json?.length || 0); - } catch (err) { - console.debug('[DnD] drop folder parse failed', node.name, err); - } + } catch {} setIsDragOver(false); }} boxShadow={isDragOver ? 'inset 0 0 0 1px var(--chakra-colors-blue-400)' : 'none'} @@ -240,7 +236,6 @@ const PlaylistItem: React.FC = React.memo(({ try { e.dataTransfer.dropEffect = 'copy'; } catch {} setIsDragOver(true); } - console.debug('[DnD] dragover playlist types', node.name, types); }} onDragLeave={() => setIsDragOver(false)} onDrop={(e) => { @@ -252,25 +247,11 @@ const PlaylistItem: React.FC = React.memo(({ let parsed: any = null; if (json && json.trim().length > 0) { parsed = JSON.parse(json); - } else if ((window as any).__rbDragPayload) { - parsed = (window as any).__rbDragPayload; } - console.debug('[DnD] drop playlist handler presence', node.name, Boolean(onDropSongs)); if (parsed?.type === 'songs' && Array.isArray(parsed.songIds) && onDropSongs) { - console.debug('[DnD] invoking onDropSongs', node.name, parsed.songIds.length); onDropSongs(node.name, parsed.songIds); - } else { - console.debug('[DnD] drop playlist no action', node.name, { - hasParsed: Boolean(parsed), - type: parsed?.type, - isArray: Array.isArray(parsed?.songIds), - hasHandler: Boolean(onDropSongs) - }); } - console.debug('[DnD] drop playlist', node.name, parsed, 'raw len', json?.length || 0); - } catch (err) { - console.debug('[DnD] drop playlist parse failed', node.name, err); - } + } catch {} setIsDragOver(false); }} boxShadow={isDragOver ? 'inset 0 0 0 1px var(--chakra-colors-blue-400)' : 'none'} @@ -366,6 +347,7 @@ export const PlaylistManager: React.FC = ({ onPlaylistDelete, onFolderCreate, onPlaylistMove, + onDropSongs, }) => { const { isOpen: isPlaylistModalOpen, onOpen: onPlaylistModalOpen, onClose: onPlaylistModalClose } = useDisclosure(); const { isOpen: isFolderModalOpen, onOpen: onFolderModalOpen, onClose: onFolderModalClose } = useDisclosure();