diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx
index 7df6c84..b36f486 100644
--- a/packages/frontend/src/App.tsx
+++ b/packages/frontend/src/App.tsx
@@ -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}
@@ -621,7 +629,14 @@ const RekordboxReader: React.FC = () => {
_hover={{ color: "blue.300", bg: "whiteAlpha.200" }}
/>
-
+
{playlistManager}
@@ -636,7 +651,14 @@ const RekordboxReader: React.FC = () => {
overflow="hidden"
>
{/* Song List */}
-
+
{
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)' },
}}
>
diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx
index edd6512..0274ff7 100644
--- a/packages/frontend/src/components/PaginatedSongList.tsx
+++ b/packages/frontend/src/components/PaginatedSongList.tsx
@@ -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(isSelected);
+ useEffect(() => {
+ setLocalChecked(isSelected);
+ }, [isSelected]);
const handleClick = useCallback(() => {
onSelect(song);
}, [onSelect, song]);
const handleCheckboxClick = useCallback((e: React.ChangeEvent) => {
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 && (
e.stopPropagation()}
@@ -176,6 +188,7 @@ export const PaginatedSongList: React.FC = memo(({
const [dragHoverIndex, setDragHoverIndex] = useState(null);
const [endDropHover, setEndDropHover] = useState(false);
const [isReorderDragging, setIsReorderDragging] = useState(false);
+ const lastSelectedIndexRef = useRef(null);
// Store current values in refs to avoid stale closures
const hasMoreRef = useRef(hasMore);
@@ -211,31 +224,60 @@ export const PaginatedSongList: React.FC = 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 = 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)'
+ }
+ }}
>
@@ -484,6 +545,8 @@ export const PaginatedSongList: React.FC = memo(({
onSelect={handleSongSelect}
onToggleSelection={toggleSelection}
showCheckbox={selectedSongs.size > 0 || depth === 0}
+ index={index}
+ onCheckboxToggle={handleCheckboxToggle}
onPlaySong={onPlaySong}
showDropIndicatorTop={dragHoverIndex === index}
onDragStart={handleDragStart}