feat(dnd): highlight playlist drop target; show drag count badge; refine drag payload and lifecycle
This commit is contained in:
parent
6917c22b94
commit
597c8f994f
@ -49,7 +49,7 @@ const SongItem = memo<{
|
|||||||
onToggleSelection: (songId: string) => void;
|
onToggleSelection: (songId: string) => void;
|
||||||
showCheckbox: boolean;
|
showCheckbox: boolean;
|
||||||
onPlaySong?: (song: Song) => void;
|
onPlaySong?: (song: Song) => void;
|
||||||
onDragStart: (songIds: string[]) => void;
|
onDragStart: (e: React.DragEvent, songIdsFallback: string[]) => void;
|
||||||
}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong, onDragStart }) => {
|
}>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong, onDragStart }) => {
|
||||||
// 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]);
|
||||||
@ -86,10 +86,7 @@ const SongItem = memo<{
|
|||||||
transition="background-color 0.2s"
|
transition="background-color 0.2s"
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) => {
|
onDragStart={(e) => {
|
||||||
// Mark this item as part of the drag selection using a custom type
|
onDragStart(e, [song.id]);
|
||||||
onDragStart([song.id]);
|
|
||||||
e.dataTransfer.setData('application/json', JSON.stringify({ type: 'songs', songIds: [song.id] }));
|
|
||||||
e.dataTransfer.effectAllowed = 'copyMove';
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showCheckbox && (
|
{showCheckbox && (
|
||||||
@ -155,6 +152,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
}) => {
|
}) => {
|
||||||
const [selectedSongs, setSelectedSongs] = useState<Set<string>>(new Set());
|
const [selectedSongs, setSelectedSongs] = useState<Set<string>>(new Set());
|
||||||
const dragSelectionRef = useRef<string[] | null>(null);
|
const dragSelectionRef = useRef<string[] | null>(null);
|
||||||
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery);
|
const [localSearchQuery, setLocalSearchQuery] = useState(searchQuery);
|
||||||
const { isOpen: isPlaylistModalOpen, onOpen: onPlaylistModalOpen, onClose: onPlaylistModalClose } = useDisclosure();
|
const { isOpen: isPlaylistModalOpen, onOpen: onPlaylistModalOpen, onClose: onPlaylistModalClose } = useDisclosure();
|
||||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||||
@ -237,11 +235,20 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
// Search handled inline via localSearchQuery effect
|
// Search handled inline via localSearchQuery effect
|
||||||
|
|
||||||
// Provide drag payload: if multiple selected, drag all; else drag the single item
|
// 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;
|
const ids = selectedSongs.size > 0 ? Array.from(selectedSongs) : songIdsFallback;
|
||||||
dragSelectionRef.current = ids;
|
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]);
|
}, [selectedSongs]);
|
||||||
|
|
||||||
|
const handleDragEnd = useCallback(() => {
|
||||||
|
setIsDragging(false);
|
||||||
|
dragSelectionRef.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Memoized song items to prevent unnecessary re-renders
|
// Memoized song items to prevent unnecessary re-renders
|
||||||
const songItems = useMemo(() => {
|
const songItems = useMemo(() => {
|
||||||
return songs.map(song => (
|
return songs.map(song => (
|
||||||
@ -325,6 +332,12 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" height="100%">
|
<Flex direction="column" height="100%">
|
||||||
|
{/* Global drag badge for selected count */}
|
||||||
|
{isDragging && (
|
||||||
|
<Box position="fixed" top={3} right={3} zIndex={2000} bg="blue.600" color="white" px={3} py={1} borderRadius="md" boxShadow="lg">
|
||||||
|
Dragging {dragSelectionRef.current?.length || 0} song{(dragSelectionRef.current?.length || 0) === 1 ? '' : 's'}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{/* Sticky Header */}
|
{/* Sticky Header */}
|
||||||
<Box
|
<Box
|
||||||
position="sticky"
|
position="sticky"
|
||||||
@ -426,7 +439,7 @@ 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}>
|
||||||
{songItems}
|
<Box onDragEnd={handleDragEnd}>{songItems}</Box>
|
||||||
|
|
||||||
{/* Loading indicator for infinite scroll or playlist switching */}
|
{/* Loading indicator for infinite scroll or playlist switching */}
|
||||||
{(loading || isSwitchingPlaylist) && (
|
{(loading || isSwitchingPlaylist) && (
|
||||||
|
|||||||
@ -106,6 +106,7 @@ const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
|||||||
onDropSongs,
|
onDropSongs,
|
||||||
}) => {
|
}) => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isDragOver, setIsDragOver] = useState(false);
|
||||||
|
|
||||||
// Memoize click handlers to prevent recreation
|
// Memoize click handlers to prevent recreation
|
||||||
const handlePlaylistClick = useCallback(() => {
|
const handlePlaylistClick = useCallback(() => {
|
||||||
@ -140,7 +141,11 @@ const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
onDragOver={(e) => e.preventDefault()}
|
onDragOver={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsDragOver(true);
|
||||||
|
}}
|
||||||
|
onDragLeave={() => setIsDragOver(false)}
|
||||||
onDrop={(e) => {
|
onDrop={(e) => {
|
||||||
try {
|
try {
|
||||||
const data = e.dataTransfer.getData('application/json');
|
const data = e.dataTransfer.getData('application/json');
|
||||||
@ -149,7 +154,10 @@ const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
|||||||
onDropSongs(node.name, parsed.songIds);
|
onDropSongs(node.name, parsed.songIds);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
setIsDragOver(false);
|
||||||
}}
|
}}
|
||||||
|
borderColor={isDragOver ? 'blue.400' : undefined}
|
||||||
|
borderWidth={isDragOver ? '1px' : undefined}
|
||||||
>
|
>
|
||||||
{level > 0 && (
|
{level > 0 && (
|
||||||
<Box
|
<Box
|
||||||
@ -209,7 +217,8 @@ const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
|||||||
borderRight="1px solid"
|
borderRight="1px solid"
|
||||||
borderRightColor="whiteAlpha.200"
|
borderRightColor="whiteAlpha.200"
|
||||||
position="relative"
|
position="relative"
|
||||||
onDragOver={(e) => e.preventDefault()}
|
onDragOver={(e) => { e.preventDefault(); setIsDragOver(true); }}
|
||||||
|
onDragLeave={() => setIsDragOver(false)}
|
||||||
onDrop={(e) => {
|
onDrop={(e) => {
|
||||||
try {
|
try {
|
||||||
const data = e.dataTransfer.getData('application/json');
|
const data = e.dataTransfer.getData('application/json');
|
||||||
@ -218,7 +227,10 @@ const PlaylistItem: React.FC<PlaylistItemProps> = React.memo(({
|
|||||||
onDropSongs(node.name, parsed.songIds);
|
onDropSongs(node.name, parsed.songIds);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
setIsDragOver(false);
|
||||||
}}
|
}}
|
||||||
|
borderColor={isDragOver ? 'blue.400' : undefined}
|
||||||
|
borderWidth={isDragOver ? '1px' : undefined}
|
||||||
>
|
>
|
||||||
{level > 0 && (
|
{level > 0 && (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user