diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 6ebdd9a..2b0dcaf 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -58,7 +58,9 @@ const SongItem = memo<{ onRowDragOver?: (e: React.DragEvent) => void; onRowDrop?: (e: React.DragEvent) => void; onRowDragStartCapture?: (e: React.DragEvent) => void; -}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong, showDropIndicatorTop, onDragStart, onRowDragOver, onRowDrop, onRowDragStartCapture }) => { + index: number; + onCheckboxToggle?: (index: number, checked: boolean, shift: boolean) => void; +}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong, showDropIndicatorTop, onDragStart, onRowDragOver, onRowDrop, onRowDragStartCapture, index, onCheckboxToggle }) => { // Memoize the formatted duration to prevent recalculation const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]); // Local optimistic selection for instant visual feedback @@ -73,8 +75,12 @@ const SongItem = memo<{ const handleCheckboxClick = useCallback((e: React.ChangeEvent) => { e.stopPropagation(); setLocalChecked(e.target.checked); - onToggleSelection(song.id); - }, [onToggleSelection, song.id]); + if (onCheckboxToggle) { + onCheckboxToggle(index, e.target.checked, (e as any).nativeEvent?.shiftKey === true || (e as any).shiftKey === true); + } else { + onToggleSelection(song.id); + } + }, [onCheckboxToggle, index, onToggleSelection, song.id]); const handlePlayClick = useCallback((e: React.MouseEvent) => { e.stopPropagation(); @@ -182,6 +188,7 @@ export const PaginatedSongList: React.FC = memo(({ const [dragHoverIndex, setDragHoverIndex] = useState(null); const [endDropHover, setEndDropHover] = useState(false); const [isReorderDragging, setIsReorderDragging] = useState(false); + const lastSelectedIndexRef = useRef(null); // Store current values in refs to avoid stale closures const hasMoreRef = useRef(hasMore); @@ -228,6 +235,32 @@ export const PaginatedSongList: React.FC = memo(({ }); }, []); + // Range selection using shift-click between last selected and current + const toggleSelectionRange = useCallback((fromIndex: number, toIndex: number, checked: boolean) => { + if (fromIndex === null || toIndex === null) return; + const [start, end] = fromIndex < toIndex ? [fromIndex, toIndex] : [toIndex, fromIndex]; + setSelectedSongs(prev => { + const next = new Set(prev); + for (let i = start; i <= end; i++) { + const id = songs[i]?.id; + if (!id) continue; + if (checked) next.add(id); else next.delete(id); + } + return next; + }); + }, [songs]); + + const handleCheckboxToggle = useCallback((index: number, checked: boolean, shift: boolean) => { + const song = songs[index]; + if (!song) return; + if (shift && lastSelectedIndexRef.current !== null) { + toggleSelectionRange(lastSelectedIndexRef.current, index, checked); + } else { + toggleSelection(song.id); + } + lastSelectedIndexRef.current = index; + }, [songs, toggleSelection, toggleSelectionRange]); + const toggleSelectAll = useCallback(() => { setSelectedSongs(prev => { const noneSelected = prev.size === 0; @@ -490,6 +523,8 @@ export const PaginatedSongList: React.FC = memo(({ onSelect={handleSongSelect} onToggleSelection={toggleSelection} showCheckbox={selectedSongs.size > 0 || depth === 0} + index={index} + onCheckboxToggle={handleCheckboxToggle} onPlaySong={onPlaySong} showDropIndicatorTop={dragHoverIndex === index} onDragStart={handleDragStart}