Merge branch 'feat/multiselect-perf' into main
This commit is contained in:
commit
febfb638b9
@ -587,6 +587,14 @@ const RekordboxReader: React.FC = () => {
|
||||
borderColor="gray.700"
|
||||
overflowY="auto"
|
||||
bg="gray.900"
|
||||
sx={{
|
||||
scrollbarColor: 'var(--chakra-colors-gray-700) var(--chakra-colors-gray-900)',
|
||||
scrollbarWidth: 'thin',
|
||||
'&::-webkit-scrollbar': { width: '8px', backgroundColor: 'var(--chakra-colors-gray-900)' },
|
||||
'&::-webkit-scrollbar-thumb': { backgroundColor: 'var(--chakra-colors-gray-700)', borderRadius: '8px' },
|
||||
'&::-webkit-scrollbar-thumb:hover': { backgroundColor: 'var(--chakra-colors-gray-600)' },
|
||||
'&::-webkit-scrollbar-track': { backgroundColor: 'var(--chakra-colors-gray-900)' }
|
||||
}}
|
||||
>
|
||||
{playlistManager}
|
||||
<ResizeHandle onMouseDown={handleResizeStart} />
|
||||
@ -621,7 +629,14 @@ const RekordboxReader: React.FC = () => {
|
||||
_hover={{ color: "blue.300", bg: "whiteAlpha.200" }}
|
||||
/>
|
||||
</DrawerHeader>
|
||||
<DrawerBody p={2}>
|
||||
<DrawerBody p={2} sx={{
|
||||
scrollbarColor: 'var(--chakra-colors-gray-700) var(--chakra-colors-gray-900)',
|
||||
scrollbarWidth: 'thin',
|
||||
'&::-webkit-scrollbar': { width: '8px', backgroundColor: 'var(--chakra-colors-gray-900)' },
|
||||
'&::-webkit-scrollbar-thumb': { backgroundColor: 'var(--chakra-colors-gray-700)', borderRadius: '8px' },
|
||||
'&::-webkit-scrollbar-thumb:hover': { backgroundColor: 'var(--chakra-colors-gray-600)' },
|
||||
'&::-webkit-scrollbar-track': { backgroundColor: 'var(--chakra-colors-gray-900)' }
|
||||
}}>
|
||||
{playlistManager}
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
@ -636,7 +651,14 @@ const RekordboxReader: React.FC = () => {
|
||||
overflow="hidden"
|
||||
>
|
||||
{/* Song List */}
|
||||
<Box flex={1} overflowY="auto" minH={0}>
|
||||
<Box flex={1} overflowY="auto" minH={0} sx={{
|
||||
scrollbarColor: 'var(--chakra-colors-gray-700) var(--chakra-colors-gray-900)',
|
||||
scrollbarWidth: 'thin',
|
||||
'&::-webkit-scrollbar': { width: '8px', backgroundColor: 'var(--chakra-colors-gray-900)' },
|
||||
'&::-webkit-scrollbar-thumb': { backgroundColor: 'var(--chakra-colors-gray-700)', borderRadius: '8px' },
|
||||
'&::-webkit-scrollbar-thumb:hover': { backgroundColor: 'var(--chakra-colors-gray-600)' },
|
||||
'&::-webkit-scrollbar-track': { backgroundColor: 'var(--chakra-colors-gray-900)' }
|
||||
}}>
|
||||
<PaginatedSongList
|
||||
songs={songs}
|
||||
onAddToPlaylist={handleAddSongsToPlaylist}
|
||||
@ -676,15 +698,12 @@ const RekordboxReader: React.FC = () => {
|
||||
bg="gray.900"
|
||||
minH={0}
|
||||
sx={{
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: 'gray.900',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: 'gray.700',
|
||||
borderRadius: '8px',
|
||||
},
|
||||
scrollbarColor: 'var(--chakra-colors-gray-700) var(--chakra-colors-gray-900)',
|
||||
scrollbarWidth: 'thin',
|
||||
'&::-webkit-scrollbar': { width: '8px', backgroundColor: 'var(--chakra-colors-gray-900)' },
|
||||
'&::-webkit-scrollbar-thumb': { backgroundColor: 'var(--chakra-colors-gray-700)', borderRadius: '8px' },
|
||||
'&::-webkit-scrollbar-thumb:hover': { backgroundColor: 'var(--chakra-colors-gray-600)' },
|
||||
'&::-webkit-scrollbar-track': { backgroundColor: 'var(--chakra-colors-gray-900)' },
|
||||
}}
|
||||
>
|
||||
<SongDetails song={selectedSong} />
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo, memo } from 'react';
|
||||
import React, { useState, useRef, useEffect, useCallback, useMemo, memo, startTransition } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
@ -58,17 +58,29 @@ 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
|
||||
const [localChecked, setLocalChecked] = useState<boolean>(isSelected);
|
||||
useEffect(() => {
|
||||
setLocalChecked(isSelected);
|
||||
}, [isSelected]);
|
||||
const handleClick = useCallback(() => {
|
||||
onSelect(song);
|
||||
}, [onSelect, song]);
|
||||
|
||||
const handleCheckboxClick = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation();
|
||||
onToggleSelection(song.id);
|
||||
}, [onToggleSelection, song.id]);
|
||||
setLocalChecked(e.target.checked);
|
||||
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();
|
||||
@ -103,7 +115,7 @@ const SongItem = memo<{
|
||||
)}
|
||||
{showCheckbox && (
|
||||
<Checkbox
|
||||
isChecked={isSelected}
|
||||
isChecked={localChecked}
|
||||
onChange={handleCheckboxClick}
|
||||
mr={3}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
@ -176,6 +188,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
||||
const [dragHoverIndex, setDragHoverIndex] = useState<number | null>(null);
|
||||
const [endDropHover, setEndDropHover] = useState<boolean>(false);
|
||||
const [isReorderDragging, setIsReorderDragging] = useState<boolean>(false);
|
||||
const lastSelectedIndexRef = useRef<number | null>(null);
|
||||
|
||||
// Store current values in refs to avoid stale closures
|
||||
const hasMoreRef = useRef(hasMore);
|
||||
@ -211,31 +224,60 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
||||
// const allPlaylists = useMemo(() => getAllPlaylists(playlists), [playlists, getAllPlaylists]);
|
||||
|
||||
const toggleSelection = useCallback((songId: string) => {
|
||||
setSelectedSongs(prev => {
|
||||
const newSelection = new Set(prev);
|
||||
if (newSelection.has(songId)) {
|
||||
newSelection.delete(songId);
|
||||
} else {
|
||||
newSelection.add(songId);
|
||||
}
|
||||
return newSelection;
|
||||
startTransition(() => {
|
||||
setSelectedSongs(prev => {
|
||||
const newSelection = new Set(prev);
|
||||
if (newSelection.has(songId)) {
|
||||
newSelection.delete(songId);
|
||||
} else {
|
||||
newSelection.add(songId);
|
||||
}
|
||||
return newSelection;
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 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];
|
||||
startTransition(() => {
|
||||
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;
|
||||
const allSelected = prev.size === songs.length && songs.length > 0;
|
||||
if (noneSelected) {
|
||||
// Select all from empty state
|
||||
startTransition(() => {
|
||||
setSelectedSongs(prev => {
|
||||
const noneSelected = prev.size === 0;
|
||||
const allSelected = prev.size === songs.length && songs.length > 0;
|
||||
if (noneSelected) {
|
||||
return new Set(songs.map(s => s.id));
|
||||
}
|
||||
if (allSelected) {
|
||||
return new Set();
|
||||
}
|
||||
return new Set(songs.map(s => s.id));
|
||||
}
|
||||
if (allSelected) {
|
||||
// Deselect all when everything is selected
|
||||
return new Set();
|
||||
}
|
||||
// Mixed/some selected: clear first, then select all (single state update reflects final state)
|
||||
return new Set(songs.map(s => s.id));
|
||||
});
|
||||
});
|
||||
}, [songs]);
|
||||
|
||||
@ -460,6 +502,25 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
||||
overflowY="auto"
|
||||
mt={2}
|
||||
id="song-list-container"
|
||||
sx={{
|
||||
scrollbarColor: 'var(--chakra-colors-gray-700) var(--chakra-colors-gray-900)',
|
||||
scrollbarWidth: 'thin',
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
backgroundColor: 'var(--chakra-colors-gray-900)'
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: 'var(--chakra-colors-gray-700)',
|
||||
borderRadius: '8px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb:hover': {
|
||||
backgroundColor: 'var(--chakra-colors-gray-600)'
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
backgroundColor: 'var(--chakra-colors-gray-900)'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box position="relative" height={`${rowVirtualizer.getTotalSize()}px`}>
|
||||
<Box onDragEnd={handleDragEnd}>
|
||||
@ -484,6 +545,8 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
||||
onSelect={handleSongSelect}
|
||||
onToggleSelection={toggleSelection}
|
||||
showCheckbox={selectedSongs.size > 0 || depth === 0}
|
||||
index={index}
|
||||
onCheckboxToggle={handleCheckboxToggle}
|
||||
onPlaySong={onPlaySong}
|
||||
showDropIndicatorTop={dragHoverIndex === index}
|
||||
onDragStart={handleDragStart}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user