feat(playlist-reorder): enable intra-playlist row drag&drop with landing indicator; persist order via backend
This commit is contained in:
parent
32d545959d
commit
50a486f6d8
@ -51,7 +51,10 @@ const SongItem = memo<{
|
|||||||
showCheckbox: boolean;
|
showCheckbox: boolean;
|
||||||
onPlaySong?: (song: Song) => void;
|
onPlaySong?: (song: Song) => void;
|
||||||
onDragStart: (e: React.DragEvent, songIdsFallback: string[]) => 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
|
// Memoize the formatted duration to prevent recalculation
|
||||||
const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]);
|
const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]);
|
||||||
const handleClick = useCallback(() => {
|
const handleClick = useCallback(() => {
|
||||||
@ -89,6 +92,9 @@ const SongItem = memo<{
|
|||||||
onDragStart={(e) => {
|
onDragStart={(e) => {
|
||||||
onDragStart(e, [song.id]);
|
onDragStart(e, [song.id]);
|
||||||
}}
|
}}
|
||||||
|
onDragOver={onRowDragOver}
|
||||||
|
onDrop={onRowDrop}
|
||||||
|
onDragStartCapture={onRowDragStartCapture}
|
||||||
>
|
>
|
||||||
{showCheckbox && (
|
{showCheckbox && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@ -162,7 +168,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const isTriggeringRef = useRef(false);
|
const isTriggeringRef = useRef(false);
|
||||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
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
|
// Store current values in refs to avoid stale closures
|
||||||
const hasMoreRef = useRef(hasMore);
|
const hasMoreRef = useRef(hasMore);
|
||||||
@ -267,11 +273,12 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
onPlaySong={onPlaySong}
|
onPlaySong={onPlaySong}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
// Simple playlist reordering within same list by dragging rows
|
// Simple playlist reordering within same list by dragging rows
|
||||||
onDragOver={(e: React.DragEvent) => {
|
onRowDragOver={(e: React.DragEvent) => {
|
||||||
if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return;
|
if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
setDragHoverIndex(index);
|
||||||
}}
|
}}
|
||||||
onDrop={async (e: React.DragEvent) => {
|
onRowDrop={async (e: React.DragEvent) => {
|
||||||
if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return;
|
if (!onReorder || !currentPlaylist || selectedSongs.size > 0) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const fromId = e.dataTransfer.getData('text/song-id');
|
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);
|
const [moved] = ordered.splice(fromIndex, 1);
|
||||||
ordered.splice(toIndex, 0, moved);
|
ordered.splice(toIndex, 0, moved);
|
||||||
await onReorder(ordered.map(s => s.id));
|
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
|
// Provide a simple id for intra-list reorder
|
||||||
if (!currentPlaylist || selectedSongs.size > 0) return;
|
if (!currentPlaylist || selectedSongs.size > 0) return;
|
||||||
e.dataTransfer.setData('text/song-id', song.id);
|
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"
|
id="song-list-container"
|
||||||
>
|
>
|
||||||
<Flex direction="column" gap={2}>
|
<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 indicator for infinite scroll or playlist switching */}
|
||||||
{(loading || isSwitchingPlaylist) && (
|
{(loading || isSwitchingPlaylist) && (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user