diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e130ccf..1338fa94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -349,6 +349,14 @@ Foundational work: faster reviews, narrower diffs, and a safety net under the pa * Row rendering moved into a memoized `PlaylistRow` with a stable callback bundle so virtualizer scroll updates do not re-render the full list. * Drag-and-drop reordering is preserved: drop-indicator overlay and edge auto-scroll during drags. +### Favorites — virtualized songs tracklist for large collections + +**By [@artplan1](https://github.com/artplan1), PR [#805](https://github.com/Psychotoxical/psysonic/pull/805)** + +* Opening Favorites with 10 000+ starred songs no longer mounts every row into the DOM. The songs tracklist is windowed with `@tanstack/react-virtual` on the shared app scroll viewport — same shape as the playlist virtualization fix. +* Row rendering moved into a memoized `FavoriteSongRow` with a stable callback bundle; `visibleTracks` is memoized once per filtered song list. +* Drag-out, preview, orbit, context menu, bulk select, and column picker behaviour are unchanged. + ## Changed ### Build — lazy-loaded routes and Vite chunk warnings diff --git a/src/components/favorites/FavoriteSongRow.tsx b/src/components/favorites/FavoriteSongRow.tsx new file mode 100644 index 00000000..5e069a2e --- /dev/null +++ b/src/components/favorites/FavoriteSongRow.tsx @@ -0,0 +1,144 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { AudioLines, ChevronRight, Play, Square, X } from 'lucide-react'; +import type { ColDef } from '../../utils/useTracklistColumns'; +import type { SubsonicSong } from '../../api/subsonicTypes'; +import { codecLabel } from '../../utils/componentHelpers/playlistDetailHelpers'; +import { formatLastSeen } from '../../utils/componentHelpers/userMgmtHelpers'; +import i18n from '../../i18n'; +import { formatTrackTime } from '../../utils/format/formatDuration'; +import StarRating from '../StarRating'; + +export interface FavoriteSongRowCallbacks { + activate: (song: SubsonicSong, index: number, e: React.MouseEvent) => void; + dblOrbit: (songId: string, e: React.MouseEvent) => void; + context: (song: SubsonicSong, e: React.MouseEvent) => void; + mouseDownRow: (song: SubsonicSong, e: React.MouseEvent) => void; + toggleSelect: (songId: string, index: number, shift: boolean) => void; + play: (index: number) => void; + startPreview: (song: SubsonicSong) => void; + rate: (songId: string, rating: number) => void; + remove: (songId: string) => void; + navArtist: (artistId: string) => void; + navAlbum: (albumId: string) => void; +} + +interface Props { + song: SubsonicSong; + index: number; + visibleCols: ColDef[]; + gridStyle: React.CSSProperties; + showBitrate: boolean; + isActive: boolean; + showEq: boolean; + isSelected: boolean; + inSelectMode: boolean; + ratingValue: number; + isPreviewing: boolean; + previewStarted: boolean; + orbitActive: boolean; + cb: FavoriteSongRowCallbacks; +} + +function FavoriteSongRow({ + song, index: i, visibleCols, gridStyle, showBitrate, + isActive, showEq, isSelected, inSelectMode, + ratingValue, isPreviewing, previewStarted, orbitActive, cb, +}: Props) { + const { t } = useTranslation(); + + return ( +