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;
|
onRowDragOver?: (e: React.DragEvent) => void;
|
||||||
onRowDrop?: (e: React.DragEvent) => void;
|
onRowDrop?: (e: React.DragEvent) => void;
|
||||||
onRowDragStartCapture?: (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
|
// Memoize the formatted duration to prevent recalculation
|
||||||
const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]);
|
const formattedDuration = useMemo(() => formatDuration(song.totalTime || ''), [song.totalTime]);
|
||||||
// Local optimistic selection for instant visual feedback
|
// Local optimistic selection for instant visual feedback
|
||||||
@ -73,8 +75,12 @@ const SongItem = memo<{
|
|||||||
const handleCheckboxClick = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleCheckboxClick = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setLocalChecked(e.target.checked);
|
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);
|
onToggleSelection(song.id);
|
||||||
}, [onToggleSelection, song.id]);
|
}
|
||||||
|
}, [onCheckboxToggle, index, onToggleSelection, song.id]);
|
||||||
|
|
||||||
const handlePlayClick = useCallback((e: React.MouseEvent) => {
|
const handlePlayClick = useCallback((e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@ -182,6 +188,7 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
const [dragHoverIndex, setDragHoverIndex] = useState<number | null>(null);
|
const [dragHoverIndex, setDragHoverIndex] = useState<number | null>(null);
|
||||||
const [endDropHover, setEndDropHover] = useState<boolean>(false);
|
const [endDropHover, setEndDropHover] = useState<boolean>(false);
|
||||||
const [isReorderDragging, setIsReorderDragging] = useState<boolean>(false);
|
const [isReorderDragging, setIsReorderDragging] = useState<boolean>(false);
|
||||||
|
const lastSelectedIndexRef = useRef<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);
|
||||||
@ -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(() => {
|
const toggleSelectAll = useCallback(() => {
|
||||||
setSelectedSongs(prev => {
|
setSelectedSongs(prev => {
|
||||||
const noneSelected = prev.size === 0;
|
const noneSelected = prev.size === 0;
|
||||||
@ -490,6 +523,8 @@ export const PaginatedSongList: React.FC<PaginatedSongListProps> = memo(({
|
|||||||
onSelect={handleSongSelect}
|
onSelect={handleSongSelect}
|
||||||
onToggleSelection={toggleSelection}
|
onToggleSelection={toggleSelection}
|
||||||
showCheckbox={selectedSongs.size > 0 || depth === 0}
|
showCheckbox={selectedSongs.size > 0 || depth === 0}
|
||||||
|
index={index}
|
||||||
|
onCheckboxToggle={handleCheckboxToggle}
|
||||||
onPlaySong={onPlaySong}
|
onPlaySong={onPlaySong}
|
||||||
showDropIndicatorTop={dragHoverIndex === index}
|
showDropIndicatorTop={dragHoverIndex === index}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user