diff --git a/package.json b/package.json index 2eb9e0817..9a07fb164 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@electron/remote": "^2.0.10", "@radix-ui/react-popover": "^1.0.6", "@radix-ui/react-slider": "^1.1.2", + "@tanstack/react-virtual": "3.0.0-beta.54", "chardet": "^1.6.0", "classnames": "^2.3.2", "electron-store": "^8.1.0", @@ -64,7 +65,6 @@ "react-fontawesome": "^1.7.1", "react-keybinding-component": "^2.0.2", "react-router-dom": "6.14.2", - "react-virtuoso": "^4.4.1", "semver": "^7.5.4", "svg-inline-react": "^3.2.1", "zustand": "^4.3.9" diff --git a/src/renderer/components/TrackRow/TrackRow.tsx b/src/renderer/components/TrackRow/TrackRow.tsx index 83f47c690..059480cfb 100644 --- a/src/renderer/components/TrackRow/TrackRow.tsx +++ b/src/renderer/components/TrackRow/TrackRow.tsx @@ -31,6 +31,7 @@ type Props = { onDragOver?: (trackId: string, position: 'above' | 'below') => void; onDragEnd?: () => void; onDrop?: (targetTrackId: string, position: 'above' | 'below') => void; + style?: React.CSSProperties; }; export default function TrackRow(props: Props) { @@ -122,6 +123,7 @@ export default function TrackRow(props: Props) { onDragLeave={(draggable && onDragLeave) || undefined} onDrop={(draggable && onDrop) || undefined} onDragEnd={(draggable && props.onDragEnd) || undefined} + style={props.style} {...(props.isPlaying ? { 'data-is-playing': true } : {})} >
diff --git a/src/renderer/components/TracksList/TracksList.module.css b/src/renderer/components/TracksList/TracksList.module.css index dd475ada1..9dc6a1a0c 100644 --- a/src/renderer/components/TracksList/TracksList.module.css +++ b/src/renderer/components/TracksList/TracksList.module.css @@ -3,14 +3,20 @@ display: flex; flex-direction: column; flex: 1 1 auto; + height: 100%; user-select: none; } -.tracksListBody { +.tracksListScroller { overflow: auto; flex: 1 1 auto; } +.tracksListRows { + width: 100%; + position: relative; +} + .tiles { position: relative; } diff --git a/src/renderer/components/TracksList/TracksList.tsx b/src/renderer/components/TracksList/TracksList.tsx index c0244dae8..98fada238 100644 --- a/src/renderer/components/TracksList/TracksList.tsx +++ b/src/renderer/components/TracksList/TracksList.tsx @@ -2,7 +2,7 @@ import type { MenuItemConstructorOptions } from 'electron'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import Keybinding from 'react-keybinding-component'; import { useNavigate } from 'react-router-dom'; -import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'; +import { useVirtualizer } from '@tanstack/react-virtual'; import TrackRow from '../TrackRow/TrackRow'; import TracksListHeader from '../TracksListHeader/TracksListHeader'; @@ -55,9 +55,20 @@ export default function TracksList(props: Props) { const [selected, setSelected] = useState([]); const [reordered, setReordered] = useState([]); - const virtuosoRef = useRef(null); const navigate = useNavigate(); + // The scrollable element for your list + const scrollableRef = useRef(null); + + // The virtualizer + const virtualizer = useVirtualizer({ + count: tracks.length, + overscan: 10, + getScrollElement: () => scrollableRef.current, + estimateSize: () => ROW_HEIGHT, + getItemKey: (index) => tracks[index]._id, + }); + const playerAPI = usePlayerAPI(); const libraryAPI = useLibraryAPI(); const highlight = useLibraryStore((state) => state.highlightPlayingTrack); @@ -65,7 +76,7 @@ export default function TracksList(props: Props) { // Highlight playing track and scroll to it // Super-mega-hacky to use Redux for that useEffect(() => { - if (highlight === true && trackPlayingId && virtuosoRef.current) { + if (highlight === true && trackPlayingId) { setSelected([trackPlayingId]); const playingTrackIndex = tracks.findIndex( @@ -73,14 +84,12 @@ export default function TracksList(props: Props) { ); if (playingTrackIndex >= 0) { - virtuosoRef.current.scrollToIndex({ - index: playingTrackIndex, - }); + virtualizer.scrollToIndex(playingTrackIndex, { behavior: 'smooth' }); } libraryAPI.highlightPlayingTrack(false); } - }, [highlight, trackPlayingId, tracks, libraryAPI]); + }, [highlight, trackPlayingId, tracks, libraryAPI, virtualizer]); /** * Helpers @@ -117,12 +126,9 @@ export default function TracksList(props: Props) { else newSelected = [tracks[addedIndex]._id]; setSelected(newSelected); - - if (virtuosoRef.current) { - virtuosoRef.current.scrollIntoView({ index: addedIndex }); - } + virtualizer.scrollToIndex(addedIndex); }, - [selected], + [selected, virtualizer], ); const onDown = useCallback( @@ -135,12 +141,9 @@ export default function TracksList(props: Props) { else newSelected = [tracks[addedIndex]._id]; setSelected(newSelected); - - if (virtuosoRef.current) { - virtuosoRef.current.scrollIntoView({ index: addedIndex }); - } + virtualizer.scrollToIndex(addedIndex); }, - [selected], + [selected, virtualizer], ); const onKey = useCallback( @@ -485,32 +488,51 @@ export default function TracksList(props: Props) {
- { - return ( - - ); - }} - /> + {/* Scrollable element */} +
+ {/* The large inner element to hold all of the items */} +
+ {/* Only the visible items in the virtualizer, manually positioned to be in view */} + {virtualizer.getVirtualItems().map((virtualItem) => { + const track = tracks[virtualItem.index]; + return ( + + ); + })} +
+
); } diff --git a/yarn.lock b/yarn.lock index 7322df7b7..a75b0e117 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1252,6 +1252,18 @@ dependencies: defer-to-connect "^2.0.0" +"@tanstack/react-virtual@3.0.0-beta.54": + version "3.0.0-beta.54" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.0.0-beta.54.tgz#755979455adf13f2584937204a3f38703e446037" + integrity sha512-D1mDMf4UPbrtHRZZriCly5bXTBMhylslm4dhcHqTtDJ6brQcgGmk8YD9JdWBGWfGSWPKoh2x1H3e7eh+hgPXtQ== + dependencies: + "@tanstack/virtual-core" "3.0.0-beta.54" + +"@tanstack/virtual-core@3.0.0-beta.54": + version "3.0.0-beta.54" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.0.0-beta.54.tgz#12259d007911ad9fce1388385c54a9141f4ecdc4" + integrity sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g== + "@tokenizer/token@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@tokenizer/token/-/token-0.3.0.tgz#fe98a93fe789247e998c75e74e9c7c63217aa276" @@ -6262,11 +6274,6 @@ react-style-singleton@^2.2.1: invariant "^2.2.4" tslib "^2.0.0" -react-virtuoso@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/react-virtuoso/-/react-virtuoso-4.4.1.tgz#43d7ac35346c4eba947b40858b375d5844b5ae9f" - integrity sha512-QrZ0JLnZFH8ltMw6q+S7U1+V2vUcSHzoIfLRzQKSv4nMJhEdjiZ+e9PqWCI7xJiy2AmSCAgo7g1V5osuurJo2Q== - react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"