feat(playlist-reorder): enable intra-playlist row drag&drop with landing indicator; persist order via backend

This commit is contained in:
Geert Rademakes 2025-08-08 14:09:36 +02:00
parent 32d545959d
commit 50a486f6d8

View File

@ -51,7 +51,10 @@ const SongItem = memo<{
showCheckbox: boolean;
onPlaySong?: (song: Song) => void;
onDragStart: (e: React.DragEvent, songIdsFallback: string[]) => void;
}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong, onDragStart }) => {
onRowDragOver?: (e: React.DragEvent) => void;
onRowDrop?: (e: React.DragEvent) => void;
onRowDragStartCapture?: (e: React.DragEvent) => void;
}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong, onDragStart, onRowDragOver, onRowDrop, onRowDragStartCapture }) => {
// Memoize the formatted duration to prevent recalculation
const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]);
const handleClick = useCallback(() => {
@ -89,6 +92,9 @@ const SongItem = memo<{
onDragStart={(e) => {
onDragStart(e, [song.id]);
}}
onDragOver={onRowDragOver}
onDrop={onRowDrop}
onDragStartCapture={onRowDragStartCapture}
>
{showCheckbox && (
<Checkbox
@ -162,7 +168,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
const scrollContainerRef = useRef<HTMLDivElement>(null);
const isTriggeringRef = useRef(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const dragIndexRef = useRef<number | null>(null);
const [dragHoverIndex, setDragHoverIndex] = useState<number | null>(null);
// Store current values in refs to avoid stale closures
const hasMoreRef = useRef(hasMore);
@ -267,11 +273,12 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
onPlaySong={onPlaySong}
onDragStart={handleDragStart}
// Simple playlist reordering within same list by dragging rows
onDragOver={(e: React.DragEvent) => {
onRowDragOver={(e: React.DragEvent) => {
if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return;
e.preventDefault();
setDragHoverIndex(index);
}}
onDrop={async (e: React.DragEvent) => {
onRowDrop={async (e: React.DragEvent) => {
if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return;
e.preventDefault();
const fromId = e.dataTransfer.getData('text/song-id');
@ -283,12 +290,12 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
const [moved] = ordered.splice(fromIndex, 1);
ordered.splice(toIndex, 0, moved);
await onReorder(ordered.map(s => s.id));
setDragHoverIndex(null);
}}
onDragStartCapture={(e: React.DragEvent) => {
onRowDragStartCapture={(e: React.DragEvent) => {
// Provide a simple id for intra-list reorder
if (!currentPlaylist || selectedSongs.size > 0) return;
e.dataTransfer.setData('text/song-id', song.id);
dragIndexRef.current = index;
}}
/>
));
@ -467,7 +474,16 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
id="song-list-container"
>
<Flex direction="column" gap={2}>
<Box onDragEnd={handleDragEnd}>{songItems}</Box>
<Box onDragEnd={handleDragEnd}>
{songs.map((song, index) => (
<Box key={`row-${song.id}`} position="relative">
{dragHoverIndex === index && (
<Box position="absolute" top={0} left={0} right={0} height="2px" bg="blue.400" zIndex={1} />
)}
{songItems[index]}
</Box>
))}
</Box>
{/* Loading indicator for infinite scroll or playlist switching */}
{(loading || isSwitchingPlaylist) && (