From 7f0b71c4b2edec5546977872e3fdf002675e9b36 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Tue, 13 Aug 2024 18:54:44 -0700 Subject: [PATCH 1/5] [scuffed bugfix]: Update table rating/favorite when updated anywhere else Modify player store to have temporary state for favorite/rating update Add effect handler for `virtual-table` to update rating/favorite for players Note that this does not handle song grid view. Using a similar handler for gird view did not work, as it appeared to result in inconsistent state. Finally, this is probably not the optimal solution. Performance appears fine for ~20k items, but no guarantees. --- .../components/virtual-table/index.tsx | 7 +++ .../components/album-detail-content.tsx | 1 + .../album-artist-detail-content.tsx | 3 +- ...m-artist-detail-top-songs-list-content.tsx | 3 +- .../components/playlist-detail-content.tsx | 1 + .../playlist-detail-song-list-content.tsx | 3 +- .../search/components/search-content.tsx | 1 + .../components/similar-songs-list.tsx | 3 +- .../songs/components/song-list-table-view.tsx | 1 + src/renderer/hooks/use-favorite-change.ts | 43 ++++++++++++++++++ src/renderer/hooks/use-rating-change.ts | 44 +++++++++++++++++++ src/renderer/store/player.store.ts | 19 ++++++++ 12 files changed, 125 insertions(+), 4 deletions(-) create mode 100644 src/renderer/hooks/use-favorite-change.ts create mode 100644 src/renderer/hooks/use-rating-change.ts diff --git a/src/renderer/components/virtual-table/index.tsx b/src/renderer/components/virtual-table/index.tsx index 95a1bdebd..16db72959 100644 --- a/src/renderer/components/virtual-table/index.tsx +++ b/src/renderer/components/virtual-table/index.tsx @@ -42,6 +42,8 @@ import { NoteCell } from '/@/renderer/components/virtual-table/cells/note-cell'; import { RowIndexCell } from '/@/renderer/components/virtual-table/cells/row-index-cell'; import i18n from '/@/i18n/i18n'; import { formatDateAbsolute, formatDateRelative, formatSizeString } from '/@/renderer/utils/format'; +import { useTableFavoriteChange } from '/@/renderer/hooks/use-favorite-change'; +import { useTableRatingChange } from '/@/renderer/hooks/use-rating-change'; export * from './table-config-dropdown'; export * from './table-pagination'; @@ -475,6 +477,7 @@ export interface VirtualTableProps extends AgGridReactProps { pagination: TablePaginationType; setPagination: any; }; + shouldUpdateSong?: boolean; stickyHeader?: boolean; transparentHeader?: boolean; } @@ -492,6 +495,7 @@ export const VirtualTable = forwardRef( onGridReady, onGridSizeChanged, paginationProps, + shouldUpdateSong, ...rest }: VirtualTableProps, ref: Ref, @@ -506,6 +510,9 @@ export const VirtualTable = forwardRef( } }); + useTableFavoriteChange(tableRef, shouldUpdateSong || false); + useTableRatingChange(tableRef, shouldUpdateSong || false); + const defaultColumnDefs: ColDef = useMemo(() => { return { lockPinned: true, diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index 728e06081..2ff314eb2 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -452,6 +452,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP key={`table-${tableConfig.rowHeight}`} ref={tableRef} autoHeight + shouldUpdateSong stickyHeader suppressCellFocus suppressLoadingOverlay diff --git a/src/renderer/features/artists/components/album-artist-detail-content.tsx b/src/renderer/features/artists/components/album-artist-detail-content.tsx index 7224ce13b..aa29b1b2b 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx @@ -523,6 +523,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten autoFitColumns autoHeight deselectOnClickOutside + shouldUpdateSong stickyHeader suppressCellFocus suppressHorizontalScroll @@ -530,7 +531,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten suppressRowDrag columnDefs={topSongsColumnDefs} enableCellChangeFlash={false} - getRowId={(data) => data.data.uniqueId} + getRowId={(data) => data.data.id} rowData={topSongs} rowHeight={60} rowSelection="multiple" diff --git a/src/renderer/features/artists/components/album-artist-detail-top-songs-list-content.tsx b/src/renderer/features/artists/components/album-artist-detail-top-songs-list-content.tsx index a85be57a3..63ade6d45 100644 --- a/src/renderer/features/artists/components/album-artist-detail-top-songs-list-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-top-songs-list-content.tsx @@ -64,8 +64,9 @@ export const AlbumArtistDetailTopSongsListContent = ({ data.data.uniqueId} + getRowId={(data) => data.data.id} rowClassRules={rowClassRules} rowData={data} rowModelType="clientSide" diff --git a/src/renderer/features/playlists/components/playlist-detail-content.tsx b/src/renderer/features/playlists/components/playlist-detail-content.tsx index 12dc2192e..060eaaa0c 100644 --- a/src/renderer/features/playlists/components/playlist-detail-content.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-content.tsx @@ -215,6 +215,7 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps) autoFitColumns autoHeight deselectOnClickOutside + shouldUpdateSong stickyHeader suppressCellFocus suppressHorizontalScroll diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx index 298e47429..9100733fa 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx @@ -240,6 +240,7 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten key={`table-${page.display}-${page.table.rowHeight}-${server?.id}`} ref={tableRef} alwaysShowHorizontalScroll + shouldUpdateSong autoFitColumns={page.table.autoFit} columnDefs={columnDefs} context={{ @@ -248,7 +249,7 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten onCellContextMenu: handleContextMenu, status, }} - getRowId={(data) => data.data.uniqueId} + getRowId={(data) => data.data.id} infiniteInitialRowCount={checkPlaylistList.data?.totalRecordCount || 100} pagination={isPaginationEnabled} paginationAutoPageSize={isPaginationEnabled} diff --git a/src/renderer/features/search/components/search-content.tsx b/src/renderer/features/search/components/search-content.tsx index 38ffc5c9e..56c2f5ba7 100644 --- a/src/renderer/features/search/components/search-content.tsx +++ b/src/renderer/features/search/components/search-content.tsx @@ -101,6 +101,7 @@ export const SearchContent = ({ tableRef }: SearchContentProps) => { getRowId={(data) => data.data.id} infiniteInitialRowCount={25} rowClassRules={rowClassRules} + shouldUpdateSong={itemType === LibraryItem.SONG} onRowDoubleClicked={handleRowDoubleClick} /> diff --git a/src/renderer/features/similar-songs/components/similar-songs-list.tsx b/src/renderer/features/similar-songs/components/similar-songs-list.tsx index fc96dd906..5089120d8 100644 --- a/src/renderer/features/similar-songs/components/similar-songs-list.tsx +++ b/src/renderer/features/similar-songs/components/similar-songs-list.tsx @@ -61,6 +61,7 @@ export const SimilarSongsList = ({ count, fullScreen, song }: SimilarSongsListPr data.data.uniqueId} + getRowId={(data) => data.data.id} rowBuffer={50} rowData={songQuery.data ?? []} rowHeight={tableConfig.rowHeight || 40} diff --git a/src/renderer/features/songs/components/song-list-table-view.tsx b/src/renderer/features/songs/components/song-list-table-view.tsx index 701bc38a9..ff498dd3a 100644 --- a/src/renderer/features/songs/components/song-list-table-view.tsx +++ b/src/renderer/features/songs/components/song-list-table-view.tsx @@ -56,6 +56,7 @@ export const SongListTableView = ({ tableRef, itemCount }: SongListTableViewProp key={`table-${tableProps.rowHeight}-${server?.id}`} ref={tableRef} {...tableProps} + shouldUpdateSong context={{ ...tableProps.context, currentSong, diff --git a/src/renderer/hooks/use-favorite-change.ts b/src/renderer/hooks/use-favorite-change.ts new file mode 100644 index 000000000..f91add942 --- /dev/null +++ b/src/renderer/hooks/use-favorite-change.ts @@ -0,0 +1,43 @@ +import { MutableRefObject, useCallback, useEffect } from 'react'; +import { usePlayerStore } from '/@/renderer/store'; +import type { AgGridReact } from '@ag-grid-community/react'; + +export const useFavoriteChange = ( + handler: (ids: string[], favorite: boolean) => void, + enabled: boolean, +) => { + useEffect(() => { + if (!enabled) return () => {}; + + const unSubChange = usePlayerStore.subscribe( + (state) => state.favorite, + (value) => value && handler(value.ids, value.favorite), + ); + + return () => { + unSubChange(); + }; + }, [handler, enabled]); +}; + +export const useTableFavoriteChange = ( + tableRef: MutableRefObject, + enabled: boolean, +) => { + const handler = useCallback( + (ids: string[], favorite: boolean) => { + const api = tableRef.current?.api; + if (api) { + for (const id of ids) { + const node = api.getRowNode(id); + if (node && node.data.userFavorite !== favorite) { + node.setDataValue('userFavorite', favorite); + } + } + } + }, + [tableRef], + ); + + useFavoriteChange(handler, enabled); +}; diff --git a/src/renderer/hooks/use-rating-change.ts b/src/renderer/hooks/use-rating-change.ts new file mode 100644 index 000000000..d9c5cd6cf --- /dev/null +++ b/src/renderer/hooks/use-rating-change.ts @@ -0,0 +1,44 @@ +import { MutableRefObject, useCallback, useEffect } from 'react'; +import { usePlayerStore } from '/@/renderer/store'; +import type { AgGridReact } from '@ag-grid-community/react'; + +export const useRatingChange = ( + handler: (ids: string[], rating: number | null) => void, + enabled: boolean, +) => { + useEffect(() => { + if (!enabled) return () => {}; + + const unSubChange = usePlayerStore.subscribe( + (state) => state.rating, + (value) => value && handler(value.ids, value.rating), + ); + + return () => { + unSubChange(); + }; + }, [enabled, handler]); +}; + +export const useTableRatingChange = ( + tableRef: MutableRefObject, + enabled: boolean, +) => { + const handler = useCallback( + (ids: string[], rating: number | null) => { + const api = tableRef.current?.api; + if (api) { + for (const id of ids) { + const node = api.getRowNode(id); + if (node && node.data.userRating !== rating) { + node.setDataValue('userRating', rating); + api.redrawRows({ rowNodes: [node] }); + } + } + } + }, + [tableRef], + ); + + useRatingChange(handler, enabled); +}; diff --git a/src/renderer/store/player.store.ts b/src/renderer/store/player.store.ts index 6e2a8286e..f9c537134 100644 --- a/src/renderer/store/player.store.ts +++ b/src/renderer/store/player.store.ts @@ -23,6 +23,10 @@ export interface PlayerState { time: number; }; fallback: boolean | null; + favorite?: { + favorite: boolean; + ids: string[]; + }; muted: boolean; queue: { default: QueueSong[]; @@ -30,6 +34,10 @@ export interface PlayerState { shuffled: string[]; sorted: QueueSong[]; }; + rating?: { + ids: string[]; + rating: number | null; + }; repeat: PlayerRepeat; shuffle: PlayerShuffle; volume: number; @@ -847,6 +855,13 @@ export const usePlayerStore = create()( }); } + set((state) => { + state.favorite = { + favorite, + ids, + }; + }); + return foundUniqueIds; }, setMuted: (muted: boolean) => { @@ -877,6 +892,10 @@ export const usePlayerStore = create()( }); } + set((state) => { + state.rating = { ids, rating }; + }); + return foundUniqueIds; }, setRepeat: (type: PlayerRepeat) => { From 030b6bf8d9bfe4cced255dd9e0ff822ffd54123e Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Sun, 1 Sep 2024 18:29:55 -0700 Subject: [PATCH 2/5] restore should update song --- .../features/artists/components/album-artist-detail-content.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/renderer/features/artists/components/album-artist-detail-content.tsx b/src/renderer/features/artists/components/album-artist-detail-content.tsx index 17171558c..96d57f4cc 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx @@ -560,6 +560,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten autoFitColumns autoHeight deselectOnClickOutside + shouldUpdateSong stickyHeader suppressCellFocus suppressHorizontalScroll From c6f4eeb1987a77fd4094014153f3a60991c7289d Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Sun, 1 Sep 2024 22:12:44 -0700 Subject: [PATCH 3/5] update song rating/favorite/played everywhere except playlist --- .../virtual-table/hooks/use-rating.ts | 130 ------------------ .../components/virtual-table/index.tsx | 6 +- .../player/mutations/scrobble-mutation.ts | 3 + .../playlist-detail-song-list-content.tsx | 3 +- .../mutations/create-favorite-mutation.ts | 3 + .../mutations/delete-favorite-mutation.ts | 3 + .../shared/mutations/set-rating-mutation.ts | 19 ++- .../songs/components/song-list-grid-view.tsx | 35 ++++- src/renderer/hooks/use-favorite-change.ts | 43 ------ src/renderer/hooks/use-rating-change.ts | 44 ------ src/renderer/store/player.store.ts | 19 --- 11 files changed, 61 insertions(+), 247 deletions(-) delete mode 100644 src/renderer/components/virtual-table/hooks/use-rating.ts delete mode 100644 src/renderer/hooks/use-favorite-change.ts delete mode 100644 src/renderer/hooks/use-rating-change.ts diff --git a/src/renderer/components/virtual-table/hooks/use-rating.ts b/src/renderer/components/virtual-table/hooks/use-rating.ts deleted file mode 100644 index 8a2508a72..000000000 --- a/src/renderer/components/virtual-table/hooks/use-rating.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { useQueryClient, useMutation } from '@tanstack/react-query'; -import { AxiosError } from 'axios'; -import { api } from '/@/renderer/api'; -import { NDAlbumDetail, NDAlbumArtistDetail } from '/@/renderer/api/navidrome.types'; -import { queryKeys } from '/@/renderer/api/query-keys'; -import { SSAlbumDetail, SSAlbumArtistDetail } from '/@/renderer/api/subsonic.types'; -import { - SetRatingArgs, - Album, - AlbumArtist, - LibraryItem, - AnyLibraryItems, - RatingResponse, - ServerType, -} from '/@/renderer/api/types'; -import { useSetAlbumListItemDataById, useSetQueueRating, getServerById } from '/@/renderer/store'; - -export const useUpdateRating = () => { - const queryClient = useQueryClient(); - const setAlbumListData = useSetAlbumListItemDataById(); - const setQueueRating = useSetQueueRating(); - - return useMutation< - RatingResponse, - AxiosError, - Omit, - { previous: { items: AnyLibraryItems } | undefined } - >({ - mutationFn: (args) => { - const server = getServerById(args.serverId); - if (!server) throw new Error('Server not found'); - return api.controller.updateRating({ ...args, apiClientProps: { server } }); - }, - onError: (_error, _variables, context) => { - for (const item of context?.previous?.items || []) { - switch (item.itemType) { - case LibraryItem.ALBUM: - setAlbumListData(item.id, { userRating: item.userRating }); - break; - case LibraryItem.SONG: - setQueueRating([item.id], item.userRating); - break; - } - } - }, - onMutate: (variables) => { - for (const item of variables.query.item) { - switch (item.itemType) { - case LibraryItem.ALBUM: - setAlbumListData(item.id, { userRating: variables.query.rating }); - break; - case LibraryItem.SONG: - setQueueRating([item.id], variables.query.rating); - break; - } - } - - return { previous: { items: variables.query.item } }; - }, - onSuccess: (_data, variables) => { - // We only need to set if we're already on the album detail page - const isAlbumDetailPage = - variables.query.item.length === 1 && - variables.query.item[0].itemType === LibraryItem.ALBUM; - - if (isAlbumDetailPage) { - const { serverType, id: albumId, serverId } = variables.query.item[0] as Album; - - const queryKey = queryKeys.albums.detail(serverId || '', { id: albumId }); - const previous = queryClient.getQueryData(queryKey); - if (previous) { - switch (serverType) { - case ServerType.NAVIDROME: - queryClient.setQueryData(queryKey, { - ...previous, - userRating: variables.query.rating, - }); - break; - case ServerType.SUBSONIC: - queryClient.setQueryData(queryKey, { - ...previous, - userRating: variables.query.rating, - }); - break; - case ServerType.JELLYFIN: - // Jellyfin does not support ratings - break; - } - } - } - - // We only need to set if we're already on the album detail page - const isAlbumArtistDetailPage = - variables.query.item.length === 1 && - variables.query.item[0].itemType === LibraryItem.ALBUM_ARTIST; - - if (isAlbumArtistDetailPage) { - const { - serverType, - id: albumArtistId, - serverId, - } = variables.query.item[0] as AlbumArtist; - - const queryKey = queryKeys.albumArtists.detail(serverId || '', { - id: albumArtistId, - }); - const previous = queryClient.getQueryData(queryKey); - if (previous) { - switch (serverType) { - case ServerType.NAVIDROME: - queryClient.setQueryData(queryKey, { - ...previous, - userRating: variables.query.rating, - }); - break; - case ServerType.SUBSONIC: - queryClient.setQueryData(queryKey, { - ...previous, - userRating: variables.query.rating, - }); - break; - case ServerType.JELLYFIN: - // Jellyfin does not support ratings - break; - } - } - } - }, - }); -}; diff --git a/src/renderer/components/virtual-table/index.tsx b/src/renderer/components/virtual-table/index.tsx index 16db72959..c21bbe0ca 100644 --- a/src/renderer/components/virtual-table/index.tsx +++ b/src/renderer/components/virtual-table/index.tsx @@ -42,8 +42,7 @@ import { NoteCell } from '/@/renderer/components/virtual-table/cells/note-cell'; import { RowIndexCell } from '/@/renderer/components/virtual-table/cells/row-index-cell'; import i18n from '/@/i18n/i18n'; import { formatDateAbsolute, formatDateRelative, formatSizeString } from '/@/renderer/utils/format'; -import { useTableFavoriteChange } from '/@/renderer/hooks/use-favorite-change'; -import { useTableRatingChange } from '/@/renderer/hooks/use-rating-change'; +import { useTableChange } from '/@/renderer/hooks/use-song-change'; export * from './table-config-dropdown'; export * from './table-pagination'; @@ -510,8 +509,7 @@ export const VirtualTable = forwardRef( } }); - useTableFavoriteChange(tableRef, shouldUpdateSong || false); - useTableRatingChange(tableRef, shouldUpdateSong || false); + useTableChange(tableRef, shouldUpdateSong === true); const defaultColumnDefs: ColDef = useMemo(() => { return { diff --git a/src/renderer/features/player/mutations/scrobble-mutation.ts b/src/renderer/features/player/mutations/scrobble-mutation.ts index 77dc7251b..714c937bf 100644 --- a/src/renderer/features/player/mutations/scrobble-mutation.ts +++ b/src/renderer/features/player/mutations/scrobble-mutation.ts @@ -4,9 +4,11 @@ import { api } from '/@/renderer/api'; import { ScrobbleResponse, ScrobbleArgs } from '/@/renderer/api/types'; import { MutationOptions } from '/@/renderer/lib/react-query'; import { getServerById, useIncrementQueuePlayCount } from '/@/renderer/store'; +import { usePlayEvent } from '/@/renderer/store/event.store'; export const useSendScrobble = (options?: MutationOptions) => { const incrementPlayCount = useIncrementQueuePlayCount(); + const sendPlayEvent = usePlayEvent(); return useMutation< ScrobbleResponse, @@ -23,6 +25,7 @@ export const useSendScrobble = (options?: MutationOptions) => { // Manually increment the play count for the song in the queue if scrobble was submitted if (variables.query.submission) { incrementPlayCount([variables.query.id]); + sendPlayEvent([variables.query.id]); } }, ...options, diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx index afdeefd0f..5954f6b0a 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx @@ -279,7 +279,6 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten key={`table-${page.display}-${page.table.rowHeight}-${server?.id}`} ref={tableRef} alwaysShowHorizontalScroll - shouldUpdateSong autoFitColumns={page.table.autoFit} columnDefs={columnDefs} context={{ @@ -288,7 +287,7 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten onCellContextMenu: handleContextMenu, status, }} - getRowId={(data) => data.data.id} + getRowId={(data) => data.data.uniqueId} infiniteInitialRowCount={checkPlaylistList.data?.totalRecordCount || 100} pagination={isPaginationEnabled} paginationAutoPageSize={isPaginationEnabled} diff --git a/src/renderer/features/shared/mutations/create-favorite-mutation.ts b/src/renderer/features/shared/mutations/create-favorite-mutation.ts index 7364c630d..8a811ff18 100644 --- a/src/renderer/features/shared/mutations/create-favorite-mutation.ts +++ b/src/renderer/features/shared/mutations/create-favorite-mutation.ts @@ -12,6 +12,7 @@ import { import { MutationHookArgs } from '/@/renderer/lib/react-query'; import { getServerById, useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store'; import isElectron from 'is-electron'; +import { useFavoriteEvent } from '/@/renderer/store/event.store'; const remote = isElectron() ? window.electron.remote : null; @@ -20,6 +21,7 @@ export const useCreateFavorite = (args: MutationHookArgs) => { const queryClient = useQueryClient(); const setAlbumListData = useSetAlbumListItemDataById(); const setQueueFavorite = useSetQueueFavorite(); + const setFavoriteEvent = useFavoriteEvent(); return useMutation< FavoriteResponse, @@ -47,6 +49,7 @@ export const useCreateFavorite = (args: MutationHookArgs) => { if (variables.query.type === LibraryItem.SONG) { remote?.updateFavorite(true, serverId, variables.query.id); setQueueFavorite(variables.query.id, true); + setFavoriteEvent(variables.query.id, true); } // We only need to set if we're already on the album detail page diff --git a/src/renderer/features/shared/mutations/delete-favorite-mutation.ts b/src/renderer/features/shared/mutations/delete-favorite-mutation.ts index bb80abb8d..a02757049 100644 --- a/src/renderer/features/shared/mutations/delete-favorite-mutation.ts +++ b/src/renderer/features/shared/mutations/delete-favorite-mutation.ts @@ -12,6 +12,7 @@ import { import { MutationHookArgs } from '/@/renderer/lib/react-query'; import { getServerById, useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store'; import isElectron from 'is-electron'; +import { useFavoriteEvent } from '/@/renderer/store/event.store'; const remote = isElectron() ? window.electron.remote : null; @@ -20,6 +21,7 @@ export const useDeleteFavorite = (args: MutationHookArgs) => { const queryClient = useQueryClient(); const setAlbumListData = useSetAlbumListItemDataById(); const setQueueFavorite = useSetQueueFavorite(); + const setFavoriteEvent = useFavoriteEvent(); return useMutation< FavoriteResponse, @@ -47,6 +49,7 @@ export const useDeleteFavorite = (args: MutationHookArgs) => { if (variables.query.type === LibraryItem.SONG) { remote?.updateFavorite(false, serverId, variables.query.id); setQueueFavorite(variables.query.id, false); + setFavoriteEvent(variables.query.id, false); } // We only need to set if we're already on the album detail page diff --git a/src/renderer/features/shared/mutations/set-rating-mutation.ts b/src/renderer/features/shared/mutations/set-rating-mutation.ts index 624230891..db3094320 100644 --- a/src/renderer/features/shared/mutations/set-rating-mutation.ts +++ b/src/renderer/features/shared/mutations/set-rating-mutation.ts @@ -15,6 +15,7 @@ import { import { MutationHookArgs } from '/@/renderer/lib/react-query'; import { getServerById, useSetAlbumListItemDataById, useSetQueueRating } from '/@/renderer/store'; import isElectron from 'is-electron'; +import { useRatingEvent } from '/@/renderer/store/event.store'; const remote = isElectron() ? window.electron.remote : null; @@ -23,6 +24,7 @@ export const useSetRating = (args: MutationHookArgs) => { const queryClient = useQueryClient(); const setAlbumListData = useSetAlbumListItemDataById(); const setQueueRating = useSetQueueRating(); + const setRatingEvent = useRatingEvent(); return useMutation< RatingResponse, @@ -43,25 +45,36 @@ export const useSetRating = (args: MutationHookArgs) => { break; case LibraryItem.SONG: setQueueRating([item.id], item.userRating); + setRatingEvent([item.id], item.userRating); break; } } }, onMutate: (variables) => { + const songIds: string[] = []; for (const item of variables.query.item) { switch (item.itemType) { case LibraryItem.ALBUM: setAlbumListData(item.id, { userRating: variables.query.rating }); break; case LibraryItem.SONG: - setQueueRating([item.id], variables.query.rating); + songIds.push(item.id); + break; } } + if (songIds.length > 0) { + setQueueRating(songIds, variables.query.rating); + setRatingEvent(songIds, variables.query.rating); + } + if (remote) { - const ids = variables.query.item.map((item) => item.id); - remote.updateRating(variables.query.rating, variables.query.item[0].serverId, ids); + remote.updateRating( + variables.query.rating, + variables.query.item[0].serverId, + songIds, + ); } return { previous: { items: variables.query.item } }; diff --git a/src/renderer/features/songs/components/song-list-grid-view.tsx b/src/renderer/features/songs/components/song-list-grid-view.tsx index 30928a4d5..8adfaddf3 100644 --- a/src/renderer/features/songs/components/song-list-grid-view.tsx +++ b/src/renderer/features/songs/components/song-list-grid-view.tsx @@ -1,5 +1,5 @@ import { QueryKey, useQueryClient } from '@tanstack/react-query'; -import { useCallback, useMemo } from 'react'; +import { MutableRefObject, useCallback, useEffect, useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; import AutoSizer, { Size } from 'react-virtualized-auto-sizer'; import { ListOnScrollProps } from 'react-window'; @@ -16,6 +16,7 @@ import { SONG_CARD_ROWS } from '/@/renderer/components'; import { VirtualGridAutoSizerContainer, VirtualInfiniteGrid, + VirtualInfiniteGridRef, } from '/@/renderer/components/virtual-grid'; import { useListContext } from '/@/renderer/context/list-context'; import { usePlayQueueAdd } from '/@/renderer/features/player'; @@ -23,8 +24,14 @@ import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store'; import { CardRow, ListDisplayType } from '/@/renderer/types'; import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite'; +import { useEventStore } from '/@/renderer/store/event.store'; -export const SongListGridView = ({ gridRef, itemCount }: any) => { +interface SongListGridViewProps { + gridRef: MutableRefObject; + itemCount?: number; +} + +export const SongListGridView = ({ gridRef, itemCount }: SongListGridViewProps) => { const queryClient = useQueryClient(); const server = useCurrentServer(); const handlePlayQueueAdd = usePlayQueueAdd(); @@ -38,6 +45,30 @@ export const SongListGridView = ({ gridRef, itemCount }: any) => { const handleFavorite = useHandleFavorite({ gridRef, server }); + useEffect(() => { + const unSub = useEventStore.subscribe((state) => { + const event = state.event; + if (event && event.event === 'favorite') { + const idSet = new Set(state.ids); + const userFavorite = event.favorite; + + gridRef.current?.updateItemData((data) => { + if (idSet.has(data.id)) { + return { + ...data, + userFavorite, + }; + } + return data; + }); + } + }); + + return () => { + unSub(); + }; + }, [gridRef]); + const cardRows = useMemo(() => { const rows: CardRow[] = [ SONG_CARD_ROWS.name, diff --git a/src/renderer/hooks/use-favorite-change.ts b/src/renderer/hooks/use-favorite-change.ts deleted file mode 100644 index f91add942..000000000 --- a/src/renderer/hooks/use-favorite-change.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { MutableRefObject, useCallback, useEffect } from 'react'; -import { usePlayerStore } from '/@/renderer/store'; -import type { AgGridReact } from '@ag-grid-community/react'; - -export const useFavoriteChange = ( - handler: (ids: string[], favorite: boolean) => void, - enabled: boolean, -) => { - useEffect(() => { - if (!enabled) return () => {}; - - const unSubChange = usePlayerStore.subscribe( - (state) => state.favorite, - (value) => value && handler(value.ids, value.favorite), - ); - - return () => { - unSubChange(); - }; - }, [handler, enabled]); -}; - -export const useTableFavoriteChange = ( - tableRef: MutableRefObject, - enabled: boolean, -) => { - const handler = useCallback( - (ids: string[], favorite: boolean) => { - const api = tableRef.current?.api; - if (api) { - for (const id of ids) { - const node = api.getRowNode(id); - if (node && node.data.userFavorite !== favorite) { - node.setDataValue('userFavorite', favorite); - } - } - } - }, - [tableRef], - ); - - useFavoriteChange(handler, enabled); -}; diff --git a/src/renderer/hooks/use-rating-change.ts b/src/renderer/hooks/use-rating-change.ts deleted file mode 100644 index d9c5cd6cf..000000000 --- a/src/renderer/hooks/use-rating-change.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { MutableRefObject, useCallback, useEffect } from 'react'; -import { usePlayerStore } from '/@/renderer/store'; -import type { AgGridReact } from '@ag-grid-community/react'; - -export const useRatingChange = ( - handler: (ids: string[], rating: number | null) => void, - enabled: boolean, -) => { - useEffect(() => { - if (!enabled) return () => {}; - - const unSubChange = usePlayerStore.subscribe( - (state) => state.rating, - (value) => value && handler(value.ids, value.rating), - ); - - return () => { - unSubChange(); - }; - }, [enabled, handler]); -}; - -export const useTableRatingChange = ( - tableRef: MutableRefObject, - enabled: boolean, -) => { - const handler = useCallback( - (ids: string[], rating: number | null) => { - const api = tableRef.current?.api; - if (api) { - for (const id of ids) { - const node = api.getRowNode(id); - if (node && node.data.userRating !== rating) { - node.setDataValue('userRating', rating); - api.redrawRows({ rowNodes: [node] }); - } - } - } - }, - [tableRef], - ); - - useRatingChange(handler, enabled); -}; diff --git a/src/renderer/store/player.store.ts b/src/renderer/store/player.store.ts index 16a3aa171..e9a89b733 100644 --- a/src/renderer/store/player.store.ts +++ b/src/renderer/store/player.store.ts @@ -23,10 +23,6 @@ export interface PlayerState { time: number; }; fallback: boolean | null; - favorite?: { - favorite: boolean; - ids: string[]; - }; muted: boolean; queue: { default: QueueSong[]; @@ -34,10 +30,6 @@ export interface PlayerState { shuffled: string[]; sorted: QueueSong[]; }; - rating?: { - ids: string[]; - rating: number | null; - }; repeat: PlayerRepeat; shuffle: PlayerShuffle; volume: number; @@ -861,13 +853,6 @@ export const usePlayerStore = create()( }); } - set((state) => { - state.favorite = { - favorite, - ids, - }; - }); - return foundUniqueIds; }, setMuted: (muted: boolean) => { @@ -898,10 +883,6 @@ export const usePlayerStore = create()( }); } - set((state) => { - state.rating = { ids, rating }; - }); - return foundUniqueIds; }, setRepeat: (type: PlayerRepeat) => { From 98a71233a8f81c8f4f70efe05323cf8fdb8bd6b1 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Sun, 1 Sep 2024 22:48:42 -0700 Subject: [PATCH 4/5] special rule for playlists --- src/remote/store/index.ts | 1 - .../components/playlist-detail-content.tsx | 26 +++++-- .../playlist-detail-song-list-content.tsx | 16 +++- .../playlists/hooks/use-scan-update.ts | 60 +++++++++++++++ src/renderer/hooks/use-song-change.ts | 76 +++++++++++++++++++ src/renderer/store/event.store.ts | 71 +++++++++++++++++ 6 files changed, 240 insertions(+), 10 deletions(-) create mode 100644 src/renderer/features/playlists/hooks/use-scan-update.ts create mode 100644 src/renderer/hooks/use-song-change.ts create mode 100644 src/renderer/store/event.store.ts diff --git a/src/remote/store/index.ts b/src/remote/store/index.ts index 6844ea23f..be0ad642b 100644 --- a/src/remote/store/index.ts +++ b/src/remote/store/index.ts @@ -175,7 +175,6 @@ export const useRemoteStore = create()( } case 'song': { set((state) => { - console.log(data); state.info.song = data; }); break; diff --git a/src/renderer/features/playlists/components/playlist-detail-content.tsx b/src/renderer/features/playlists/components/playlist-detail-content.tsx index 060eaaa0c..31038a631 100644 --- a/src/renderer/features/playlists/components/playlist-detail-content.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-content.tsx @@ -1,5 +1,5 @@ -import { MutableRefObject, useMemo, useRef } from 'react'; -import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core'; +import { MutableRefObject, useCallback, useMemo, useRef } from 'react'; +import { ColDef, GetRowIdParams, RowDoubleClickedEvent } from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { Box, Group } from '@mantine/core'; import { closeAllModals, openModal } from '@mantine/modals'; @@ -9,7 +9,7 @@ import { generatePath, useNavigate, useParams } from 'react-router'; import { Link } from 'react-router-dom'; import styled from 'styled-components'; import { useListStoreByKey } from '../../../store/list.store'; -import { LibraryItem, QueueSong } from '/@/renderer/api/types'; +import { LibraryItem, QueueSong, ServerType } from '/@/renderer/api/types'; import { Button, ConfirmModal, DropdownMenu, MotionGroup, toast } from '/@/renderer/components'; import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table'; import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles'; @@ -28,6 +28,7 @@ import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer } from '/@/renderer/store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { Play } from '/@/renderer/types'; +import { useScanUpdate } from '/@/renderer/features/playlists/hooks/use-scan-update'; const ContentContainer = styled.div` position: relative; @@ -156,6 +157,18 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps) const loadMoreRef = useRef(null); + // Duplicates are only present if on Navidrome + const getId = useCallback( + (data: GetRowIdParams): string => { + return server?.type === ServerType.JELLYFIN + ? data.data.id + : `${data.data.id}-${data.data.pageIndex}`; + }, + [server?.type], + ); + + useScanUpdate(server, tableRef); + return ( { - // It's possible that there are duplicate song ids in a playlist - return `${data.data.id}-${data.data.pageIndex}`; - }} + getRowId={getId} rowClassRules={rowClassRules} rowData={playlistSongData} rowHeight={60} rowSelection="multiple" + shouldUpdateSong={server?.type === ServerType.JELLYFIN} onCellContextMenu={handleContextMenu} onRowDoubleClicked={handleRowDoubleClick} /> diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx index 5954f6b0a..b2637bc8a 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx @@ -1,6 +1,7 @@ import type { BodyScrollEvent, ColDef, + GetRowIdParams, GridReadyEvent, IDatasource, PaginationChangedEvent, @@ -19,6 +20,7 @@ import { LibraryItem, PlaylistSongListQuery, QueueSong, + ServerType, Song, SongListSort, SortOrder, @@ -47,6 +49,7 @@ import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { ListDisplayType } from '/@/renderer/types'; import { useAppFocus } from '/@/renderer/hooks'; import { toast } from '/@/renderer/components'; +import { useScanUpdate } from '/@/renderer/features/playlists/hooks/use-scan-update'; interface PlaylistDetailContentProps { tableRef: MutableRefObject; @@ -270,6 +273,16 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten const { rowClassRules } = useCurrentSongRowStyles({ tableRef }); + // Duplicates are only present if on Navidrome + const getId = useCallback( + (data: GetRowIdParams): string => { + return server?.type === ServerType.JELLYFIN ? data.data.id : data.data.uniqueId; + }, + [server?.type], + ); + + useScanUpdate(server, tableRef); + return ( <> @@ -287,7 +300,7 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten onCellContextMenu: handleContextMenu, status, }} - getRowId={(data) => data.data.uniqueId} + getRowId={getId} infiniteInitialRowCount={checkPlaylistList.data?.totalRecordCount || 100} pagination={isPaginationEnabled} paginationAutoPageSize={isPaginationEnabled} @@ -298,6 +311,7 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten } rowHeight={page.table.rowHeight || 40} rowModelType="infinite" + shouldUpdateSong={server?.type === ServerType.JELLYFIN} onBodyScrollEnd={handleScroll} onCellContextMenu={handleContextMenu} onColumnMoved={handleColumnChange} diff --git a/src/renderer/features/playlists/hooks/use-scan-update.ts b/src/renderer/features/playlists/hooks/use-scan-update.ts new file mode 100644 index 000000000..3d4574f7b --- /dev/null +++ b/src/renderer/features/playlists/hooks/use-scan-update.ts @@ -0,0 +1,60 @@ +import { MutableRefObject, useCallback } from 'react'; +import { ServerListItem, ServerType, Song } from '/@/renderer/api/types'; +import { RowNode } from '@ag-grid-community/core'; +import { AgGridReact } from '@ag-grid-community/react'; +import { useSongChange } from '/@/renderer/hooks/use-song-change'; +import { UserEvent } from '/@/renderer/store/event.store'; + +export const useScanUpdate = ( + server: ServerListItem | null, + tableRef: MutableRefObject, +) => { + const handler = useCallback( + (ids: string[], event: UserEvent) => { + const api = tableRef.current?.api; + if (!api) return; + + const idSet = new Set(ids); + const rowNodes: RowNode[] = []; + + api.forEachNode((node: RowNode) => { + if (node.data) { + if (idSet.has(node.data.id)) { + switch (event.event) { + case 'favorite': { + if (node.data.userFavorite !== event.favorite) { + node.setDataValue('userFavorite', event.favorite); + } + break; + } + case 'play': + if (node.data.lastPlayedAt !== event.timestamp) { + node.setData({ + ...node.data, + lastPlayedAt: event.timestamp, + playCount: node.data.playCount + 1, + }); + } + node.data.lastPlayedAt = event.timestamp; + break; + case 'rating': { + if (node.data.userRating !== event.rating) { + node.setDataValue('userRating', event.rating); + rowNodes.push(node); + } + break; + } + } + } + } + }); + + if (rowNodes.length > 0) { + api.redrawRows({ rowNodes }); + } + }, + [tableRef], + ); + + useSongChange(handler, server?.type === ServerType.NAVIDROME); +}; diff --git a/src/renderer/hooks/use-song-change.ts b/src/renderer/hooks/use-song-change.ts new file mode 100644 index 000000000..3e25a3301 --- /dev/null +++ b/src/renderer/hooks/use-song-change.ts @@ -0,0 +1,76 @@ +import { MutableRefObject, useCallback, useEffect } from 'react'; +import { useEventStore, UserEvent } from '/@/renderer/store/event.store'; +import { RowNode } from '@ag-grid-community/core'; +import { AgGridReact } from '@ag-grid-community/react'; +import { Song } from '/@/renderer/api/types'; + +export const useSongChange = ( + handler: (ids: string[], event: UserEvent) => void, + enabled: boolean, +) => { + useEffect(() => { + if (!enabled) return () => {}; + + const unSub = useEventStore.subscribe((state) => { + if (state.event) { + handler(state.ids, state.event); + } + }); + + return () => { + unSub(); + }; + }, [handler, enabled]); +}; + +export const useTableChange = ( + tableRef: MutableRefObject, + enabled: boolean, +) => { + const handler = useCallback( + (ids: string[], event: UserEvent) => { + const api = tableRef.current?.api; + if (!api) return; + + const rowNodes: RowNode[] = []; + for (const id of ids) { + const node: RowNode | undefined = api.getRowNode(id); + if (node && node.data) { + switch (event.event) { + case 'favorite': { + if (node.data.userFavorite !== event.favorite) { + node.setDataValue('userFavorite', event.favorite); + } + break; + } + case 'play': + if (node.data.lastPlayedAt !== event.timestamp) { + node.setData({ + ...node.data, + lastPlayedAt: event.timestamp, + playCount: node.data.playCount + 1, + }); + } + node.data.lastPlayedAt = event.timestamp; + break; + case 'rating': { + if (node.data.userRating !== event.rating) { + node.setDataValue('userRating', event.rating); + rowNodes.push(node); + } + break; + } + } + } + } + + // This is required to redraw star rows + if (rowNodes.length > 0) { + api.redrawRows({ rowNodes }); + } + }, + [tableRef], + ); + + useSongChange(handler, enabled); +}; diff --git a/src/renderer/store/event.store.ts b/src/renderer/store/event.store.ts new file mode 100644 index 000000000..8d69bd3e4 --- /dev/null +++ b/src/renderer/store/event.store.ts @@ -0,0 +1,71 @@ +import { create } from 'zustand'; +import { devtools, subscribeWithSelector } from 'zustand/middleware'; +import { immer } from 'zustand/middleware/immer'; + +export type FavoriteEvent = { + event: 'favorite'; + favorite: boolean; +}; + +export type PlayEvent = { + event: 'play'; + timestamp: string; +}; + +export type RatingEvent = { + event: 'rating'; + rating: number | null; +}; + +export type UserEvent = FavoriteEvent | PlayEvent | RatingEvent; + +export interface EventState { + event: UserEvent | null; + ids: string[]; +} + +export interface EventSlice extends EventState { + actions: { + favorite: (ids: string[], favorite: boolean) => void; + play: (ids: string[]) => void; + rate: (ids: string[], rating: number | null) => void; + }; +} + +export const useEventStore = create()( + subscribeWithSelector( + devtools( + immer((set) => ({ + actions: { + favorite(ids, favorite) { + set((state) => { + state.event = { event: 'favorite', favorite }; + state.ids = ids; + }); + }, + play(ids) { + set((state) => { + state.event = { event: 'play', timestamp: new Date().toISOString() }; + state.ids = ids; + }); + }, + rate(ids, rating) { + set((state) => { + state.event = { event: 'rating', rating }; + state.ids = ids; + }); + }, + }, + event: null, + ids: [], + })), + { name: 'event_store' }, + ), + ), +); + +export const useFavoriteEvent = () => useEventStore((state) => state.actions.favorite); + +export const usePlayEvent = () => useEventStore((state) => state.actions.play); + +export const useRatingEvent = () => useEventStore((state) => state.actions.rate); From c7ba49b37cbd43527749f4c9244daf404b9646c2 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Mon, 2 Sep 2024 22:07:23 -0700 Subject: [PATCH 5/5] use iterator instead --- .../components/playlist-detail-content.tsx | 23 ++----- .../playlist-detail-song-list-content.tsx | 17 +----- .../playlists/hooks/use-scan-update.ts | 60 ------------------- src/renderer/hooks/use-song-change.ts | 53 ++++++++-------- 4 files changed, 34 insertions(+), 119 deletions(-) delete mode 100644 src/renderer/features/playlists/hooks/use-scan-update.ts diff --git a/src/renderer/features/playlists/components/playlist-detail-content.tsx b/src/renderer/features/playlists/components/playlist-detail-content.tsx index 31038a631..9f51c987b 100644 --- a/src/renderer/features/playlists/components/playlist-detail-content.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-content.tsx @@ -1,5 +1,5 @@ -import { MutableRefObject, useCallback, useMemo, useRef } from 'react'; -import { ColDef, GetRowIdParams, RowDoubleClickedEvent } from '@ag-grid-community/core'; +import { MutableRefObject, useMemo, useRef } from 'react'; +import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { Box, Group } from '@mantine/core'; import { closeAllModals, openModal } from '@mantine/modals'; @@ -9,7 +9,7 @@ import { generatePath, useNavigate, useParams } from 'react-router'; import { Link } from 'react-router-dom'; import styled from 'styled-components'; import { useListStoreByKey } from '../../../store/list.store'; -import { LibraryItem, QueueSong, ServerType } from '/@/renderer/api/types'; +import { LibraryItem, QueueSong } from '/@/renderer/api/types'; import { Button, ConfirmModal, DropdownMenu, MotionGroup, toast } from '/@/renderer/components'; import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table'; import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles'; @@ -28,7 +28,6 @@ import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer } from '/@/renderer/store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { Play } from '/@/renderer/types'; -import { useScanUpdate } from '/@/renderer/features/playlists/hooks/use-scan-update'; const ContentContainer = styled.div` position: relative; @@ -157,18 +156,6 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps) const loadMoreRef = useRef(null); - // Duplicates are only present if on Navidrome - const getId = useCallback( - (data: GetRowIdParams): string => { - return server?.type === ServerType.JELLYFIN - ? data.data.id - : `${data.data.id}-${data.data.pageIndex}`; - }, - [server?.type], - ); - - useScanUpdate(server, tableRef); - return ( `${data.data.id}-${data.data.pageIndex}`} rowClassRules={rowClassRules} rowData={playlistSongData} rowHeight={60} rowSelection="multiple" - shouldUpdateSong={server?.type === ServerType.JELLYFIN} onCellContextMenu={handleContextMenu} onRowDoubleClicked={handleRowDoubleClick} /> diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx index b2637bc8a..c2b6d2d48 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx @@ -1,7 +1,6 @@ import type { BodyScrollEvent, ColDef, - GetRowIdParams, GridReadyEvent, IDatasource, PaginationChangedEvent, @@ -20,7 +19,6 @@ import { LibraryItem, PlaylistSongListQuery, QueueSong, - ServerType, Song, SongListSort, SortOrder, @@ -49,7 +47,6 @@ import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { ListDisplayType } from '/@/renderer/types'; import { useAppFocus } from '/@/renderer/hooks'; import { toast } from '/@/renderer/components'; -import { useScanUpdate } from '/@/renderer/features/playlists/hooks/use-scan-update'; interface PlaylistDetailContentProps { tableRef: MutableRefObject; @@ -273,16 +270,6 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten const { rowClassRules } = useCurrentSongRowStyles({ tableRef }); - // Duplicates are only present if on Navidrome - const getId = useCallback( - (data: GetRowIdParams): string => { - return server?.type === ServerType.JELLYFIN ? data.data.id : data.data.uniqueId; - }, - [server?.type], - ); - - useScanUpdate(server, tableRef); - return ( <> @@ -292,6 +279,7 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten key={`table-${page.display}-${page.table.rowHeight}-${server?.id}`} ref={tableRef} alwaysShowHorizontalScroll + shouldUpdateSong autoFitColumns={page.table.autoFit} columnDefs={columnDefs} context={{ @@ -300,7 +288,7 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten onCellContextMenu: handleContextMenu, status, }} - getRowId={getId} + getRowId={(data) => data.data.uniqueId} infiniteInitialRowCount={checkPlaylistList.data?.totalRecordCount || 100} pagination={isPaginationEnabled} paginationAutoPageSize={isPaginationEnabled} @@ -311,7 +299,6 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten } rowHeight={page.table.rowHeight || 40} rowModelType="infinite" - shouldUpdateSong={server?.type === ServerType.JELLYFIN} onBodyScrollEnd={handleScroll} onCellContextMenu={handleContextMenu} onColumnMoved={handleColumnChange} diff --git a/src/renderer/features/playlists/hooks/use-scan-update.ts b/src/renderer/features/playlists/hooks/use-scan-update.ts deleted file mode 100644 index 3d4574f7b..000000000 --- a/src/renderer/features/playlists/hooks/use-scan-update.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { MutableRefObject, useCallback } from 'react'; -import { ServerListItem, ServerType, Song } from '/@/renderer/api/types'; -import { RowNode } from '@ag-grid-community/core'; -import { AgGridReact } from '@ag-grid-community/react'; -import { useSongChange } from '/@/renderer/hooks/use-song-change'; -import { UserEvent } from '/@/renderer/store/event.store'; - -export const useScanUpdate = ( - server: ServerListItem | null, - tableRef: MutableRefObject, -) => { - const handler = useCallback( - (ids: string[], event: UserEvent) => { - const api = tableRef.current?.api; - if (!api) return; - - const idSet = new Set(ids); - const rowNodes: RowNode[] = []; - - api.forEachNode((node: RowNode) => { - if (node.data) { - if (idSet.has(node.data.id)) { - switch (event.event) { - case 'favorite': { - if (node.data.userFavorite !== event.favorite) { - node.setDataValue('userFavorite', event.favorite); - } - break; - } - case 'play': - if (node.data.lastPlayedAt !== event.timestamp) { - node.setData({ - ...node.data, - lastPlayedAt: event.timestamp, - playCount: node.data.playCount + 1, - }); - } - node.data.lastPlayedAt = event.timestamp; - break; - case 'rating': { - if (node.data.userRating !== event.rating) { - node.setDataValue('userRating', event.rating); - rowNodes.push(node); - } - break; - } - } - } - } - }); - - if (rowNodes.length > 0) { - api.redrawRows({ rowNodes }); - } - }, - [tableRef], - ); - - useSongChange(handler, server?.type === ServerType.NAVIDROME); -}; diff --git a/src/renderer/hooks/use-song-change.ts b/src/renderer/hooks/use-song-change.ts index 3e25a3301..a6c008bb8 100644 --- a/src/renderer/hooks/use-song-change.ts +++ b/src/renderer/hooks/use-song-change.ts @@ -33,36 +33,37 @@ export const useTableChange = ( if (!api) return; const rowNodes: RowNode[] = []; - for (const id of ids) { - const node: RowNode | undefined = api.getRowNode(id); - if (node && node.data) { - switch (event.event) { - case 'favorite': { - if (node.data.userFavorite !== event.favorite) { - node.setDataValue('userFavorite', event.favorite); - } - break; + const idSet = new Set(ids); + + api.forEachNode((node: RowNode) => { + if (!node.data || !idSet.has(node.data.id)) return; + + switch (event.event) { + case 'favorite': { + if (node.data.userFavorite !== event.favorite) { + node.setDataValue('userFavorite', event.favorite); } - case 'play': - if (node.data.lastPlayedAt !== event.timestamp) { - node.setData({ - ...node.data, - lastPlayedAt: event.timestamp, - playCount: node.data.playCount + 1, - }); - } - node.data.lastPlayedAt = event.timestamp; - break; - case 'rating': { - if (node.data.userRating !== event.rating) { - node.setDataValue('userRating', event.rating); - rowNodes.push(node); - } - break; + break; + } + case 'play': + if (node.data.lastPlayedAt !== event.timestamp) { + node.setData({ + ...node.data, + lastPlayedAt: event.timestamp, + playCount: node.data.playCount + 1, + }); } + node.data.lastPlayedAt = event.timestamp; + break; + case 'rating': { + if (node.data.userRating !== event.rating) { + node.setDataValue('userRating', event.rating); + rowNodes.push(node); + } + break; } } - } + }); // This is required to redraw star rows if (rowNodes.length > 0) {