From 54b22d5cc5a9d62c19c57f14e9dcf5de6ae694a7 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Wed, 13 Aug 2025 15:57:26 +0200 Subject: [PATCH 1/5] perf(frontend): instant checkbox feedback via local optimistic selection in virtualized list --- packages/frontend/src/components/PaginatedSongList.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index edd6512..6ebdd9a 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -61,12 +61,18 @@ const SongItem = memo<{ }>(({ song, isSelected, isHighlighted, onSelect, onToggleSelection, showCheckbox, onPlaySong, showDropIndicatorTop, onDragStart, onRowDragOver, onRowDrop, onRowDragStartCapture }) => { // 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(); + setLocalChecked(e.target.checked); onToggleSelection(song.id); }, [onToggleSelection, song.id]); @@ -103,7 +109,7 @@ const SongItem = memo<{ )} {showCheckbox && ( e.stopPropagation()} From 1d290bdfa651ae59790588f0b45439cf14329c1a Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Wed, 13 Aug 2025 15:59:25 +0200 Subject: [PATCH 2/5] feat(frontend): shift-click range selection with optimistic checkbox feedback --- .../src/components/PaginatedSongList.tsx | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 6ebdd9a..2b0dcaf 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -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) => { 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 = 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); @@ -228,6 +235,32 @@ export const PaginatedSongList: React.FC = 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 = memo(({ onSelect={handleSongSelect} onToggleSelection={toggleSelection} showCheckbox={selectedSongs.size > 0 || depth === 0} + index={index} + onCheckboxToggle={handleCheckboxToggle} onPlaySong={onPlaySong} showDropIndicatorTop={dragHoverIndex === index} onDragStart={handleDragStart} From 017ba31d83942200a5de718232fb8db8ad053468 Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Wed, 13 Aug 2025 16:03:26 +0200 Subject: [PATCH 3/5] perf(frontend): use startTransition for selection updates to keep UI responsive on large sets --- .../src/components/PaginatedSongList.tsx | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/packages/frontend/src/components/PaginatedSongList.tsx b/packages/frontend/src/components/PaginatedSongList.tsx index 2b0dcaf..caf5f15 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, @@ -224,14 +224,16 @@ 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; + }); }); }, []); @@ -239,14 +241,16 @@ export const PaginatedSongList: React.FC = memo(({ 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; + 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]); @@ -262,19 +266,18 @@ export const PaginatedSongList: React.FC = memo(({ }, [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]); From dd2c7d353ef7215f7ce7ffeb197f5bf0a9481b0d Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Wed, 13 Aug 2025 16:06:02 +0200 Subject: [PATCH 4/5] style(frontend): dark scrollbars for song list, main content, and details panel to match theme --- packages/frontend/src/App.tsx | 24 +++++++++++-------- .../src/components/PaginatedSongList.tsx | 19 +++++++++++++++ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 7df6c84..c427b51 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -636,7 +636,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 caf5f15..0274ff7 100644 --- a/packages/frontend/src/components/PaginatedSongList.tsx +++ b/packages/frontend/src/components/PaginatedSongList.tsx @@ -502,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)' + } + }} > From feac54e2e0d65b4833245e31c16ed3521c8f811d Mon Sep 17 00:00:00 2001 From: Geert Rademakes Date: Wed, 13 Aug 2025 16:08:38 +0200 Subject: [PATCH 5/5] style(frontend): dark scrollbars for playlist sidebar and mobile drawer --- packages/frontend/src/App.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index c427b51..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}