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"