feat(frontend): shift-click range selection with optimistic checkbox feedback
This commit is contained in:
parent
54b22d5cc5
commit
1d290bdfa6
@ -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<HTMLInputElement>) => {
|
||||
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<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);
|
||||
@ -228,6 +235,32 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = 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<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