diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index 6aaff432..9ab138f9 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -542,10 +542,6 @@ export const JellyfinController: ControllerEndpoint = { query: { Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags', IncludeItemTypes: 'Audio', - Limit: query.limit, - SortBy: query.sortBy ? songListSortMap.jellyfin[query.sortBy] : undefined, - SortOrder: query.sortOrder ? sortOrderMap.jellyfin[query.sortOrder] : undefined, - StartIndex: query.startIndex, UserId: apiClientProps.server?.userId, }, }); @@ -556,7 +552,7 @@ export const JellyfinController: ControllerEndpoint = { return { items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server, '')), - startIndex: query.startIndex, + startIndex: 0, totalRecordCount: res.body.TotalRecordCount, }; }, diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index 27e31e54..e2641dbd 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -3,7 +3,6 @@ import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api'; import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller'; import { NDSongListSort } from '/@/shared/api/navidrome.types'; import { ndNormalize } from '/@/shared/api/navidrome/navidrome-normalize'; -import { ndType } from '/@/shared/api/navidrome/navidrome-types'; import { ssNormalize } from '/@/shared/api/subsonic/subsonic-normalize'; import { SubsonicExtensions } from '/@/shared/api/subsonic/subsonic-types'; import { getFeatures, hasFeature, VersionInfo } from '/@/shared/api/utils'; @@ -430,12 +429,9 @@ export const NavidromeController: ControllerEndpoint = { id: query.id, }, query: { - _end: query.startIndex + (query.limit || -1), - _order: query.sortOrder ? sortOrderMap.navidrome[query.sortOrder] : 'ASC', - _sort: query.sortBy - ? songListSortMap.navidrome[query.sortBy] - : ndType._enum.songList.ID, - _start: query.startIndex, + _end: -1, + _order: 'ASC', + _start: 0, ...excludeMissing(apiClientProps.server), }, }); @@ -446,7 +442,7 @@ export const NavidromeController: ControllerEndpoint = { return { items: res.body.data.map((item) => ndNormalize.song(item, apiClientProps.server)), - startIndex: query?.startIndex || 0, + startIndex: 0, totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), }; }, diff --git a/src/renderer/api/query-keys.ts b/src/renderer/api/query-keys.ts index d14444c0..ae146739 100644 --- a/src/renderer/api/query-keys.ts +++ b/src/renderer/api/query-keys.ts @@ -9,7 +9,6 @@ import type { LyricsQuery, PlaylistDetailQuery, PlaylistListQuery, - PlaylistSongListQuery, RandomSongListQuery, SearchQuery, SimilarSongsQuery, @@ -191,21 +190,6 @@ export const queryKeys: Record< if (id) return [serverId, 'playlists', id, 'detail'] as const; return [serverId, 'playlists', 'detail'] as const; }, - detailSongList: (serverId: string, id: string, query?: PlaylistSongListQuery) => { - const { filter, pagination } = splitPaginatedQuery(query); - - if (query && id && pagination) { - return [serverId, 'playlists', id, 'detailSongList', filter, pagination] as const; - } - - if (query && id) { - return [serverId, 'playlists', id, 'detailSongList', filter] as const; - } - - if (id) return [serverId, 'playlists', id, 'detailSongList'] as const; - - return [serverId, 'playlists', 'detailSongList'] as const; - }, list: (serverId: string, query?: PlaylistListQuery) => { const { filter, pagination } = splitPaginatedQuery(query); if (query && pagination) { @@ -219,16 +203,7 @@ export const queryKeys: Record< return [serverId, 'playlists', 'list'] as const; }, root: (serverId: string) => [serverId, 'playlists'] as const, - songList: (serverId: string, id?: string, query?: PlaylistSongListQuery) => { - const { filter, pagination } = splitPaginatedQuery(query); - if (query && id && pagination) { - return [serverId, 'playlists', id, 'songList', filter, pagination] as const; - } - - if (query && id) { - return [serverId, 'playlists', id, 'songList', filter] as const; - } - + songList: (serverId: string, id?: string) => { if (id) return [serverId, 'playlists', id, 'songList'] as const; return [serverId, 'playlists', 'songList'] as const; }, diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index 12fa2399..149e2b51 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -759,18 +759,14 @@ export const SubsonicController: ControllerEndpoint = { throw new Error('Failed to get playlist song list'); } - let results = + const items = res.body.playlist.entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) || []; - if (query.sortBy && query.sortOrder) { - results = sortSongList(results, query.sortBy, query.sortOrder); - } - return { - items: results, + items, startIndex: 0, - totalRecordCount: results?.length || 0, + totalRecordCount: items.length, }; }, getRandomSongList: async (args) => { diff --git a/src/renderer/features/context-menu/context-menu-provider.tsx b/src/renderer/features/context-menu/context-menu-provider.tsx index 29723e11..c1f0c836 100644 --- a/src/renderer/features/context-menu/context-menu-provider.tsx +++ b/src/renderer/features/context-menu/context-menu-provider.tsx @@ -541,7 +541,6 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { }); }, onSuccess: () => { - ctx.context?.tableRef?.current?.api?.refreshInfiniteCache(); closeAllModals(); }, }, diff --git a/src/renderer/features/lyrics/lyrics.module.css b/src/renderer/features/lyrics/lyrics.module.css index 5122fcc0..a270ea68 100644 --- a/src/renderer/features/lyrics/lyrics.module.css +++ b/src/renderer/features/lyrics/lyrics.module.css @@ -24,8 +24,8 @@ position: relative; display: flex; width: 100%; - height: 100%; min-width: 0; + height: 100%; overflow: hidden; &:hover { diff --git a/src/renderer/features/player/utils.ts b/src/renderer/features/player/utils.ts index fb4c58f9..048cc212 100644 --- a/src/renderer/features/player/utils.ts +++ b/src/renderer/features/player/utils.ts @@ -4,17 +4,19 @@ import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; import { PlaylistSongListQuery, + PlaylistSongListQueryClientSide, ServerListItem, SongDetailQuery, SongListQuery, SongListResponse, SongListSort, SortOrder, + sortSongList, } from '/@/shared/types/domain-types'; export const getPlaylistSongsById = async (args: { id: string; - query?: Partial; + query?: Partial; queryClient: QueryClient; server: ServerListItem; }) => { @@ -22,13 +24,9 @@ export const getPlaylistSongsById = async (args: { const queryFilter: PlaylistSongListQuery = { id, - sortBy: SongListSort.ID, - sortOrder: SortOrder.ASC, - startIndex: 0, - ...query, }; - const queryKey = queryKeys.playlists.songList(server?.id, id, queryFilter); + const queryKey = queryKeys.playlists.songList(server?.id, id); const res = await queryClient.fetchQuery( queryKey, @@ -46,6 +44,14 @@ export const getPlaylistSongsById = async (args: { }, ); + if (res) { + res.items = sortSongList( + res.items, + query?.sortBy || SongListSort.ID, + query?.sortOrder || SortOrder.ASC, + ); + } + return res; }; diff --git a/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx b/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx index 4732a758..9bae51c0 100644 --- a/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx +++ b/src/renderer/features/playlists/components/add-to-playlist-context-modal.tsx @@ -145,12 +145,7 @@ export const AddToPlaylistContextModal = ({ const uniqueSongIds: string[] = []; if (values.skipDuplicates) { - const query = { - id: playlistId, - startIndex: 0, - }; - - const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId, query); + const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId); const playlistSongsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => { if (!server) @@ -164,9 +159,6 @@ export const AddToPlaylistContextModal = ({ }, query: { id: playlistId, - sortBy: SongListSort.ID, - sortOrder: SortOrder.ASC, - startIndex: 0, }, }); }); 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 8caedb55..14f7c902 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 @@ -2,7 +2,6 @@ import type { BodyScrollEvent, ColDef, GridReadyEvent, - IDatasource, PaginationChangedEvent, RowDoubleClickedEvent, RowDragEvent, @@ -27,7 +26,6 @@ import { } from '/@/renderer/features/context-menu/context-menu-items'; import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query'; -import { usePlaylistSongList } from '/@/renderer/features/playlists/queries/playlist-song-list-query'; import { useAppFocus } from '/@/renderer/hooks'; import { useCurrentServer, @@ -42,13 +40,15 @@ import { PersistedTableColumn, usePlayButtonBehavior } from '/@/renderer/store/s import { toast } from '/@/shared/components/toast/toast'; import { LibraryItem, - PlaylistSongListQuery, + PlaylistSongListQueryClientSide, QueueSong, + ServerType, Song, + SongListResponse, SongListSort, SortOrder, } from '/@/shared/types/domain-types'; -import { ListDisplayType, ServerType } from '/@/shared/types/types'; +import { ListDisplayType } from '/@/shared/types/types'; interface PlaylistDetailContentProps { songs?: Song[]; @@ -63,7 +63,7 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai const currentSong = useCurrentSong(); const server = useCurrentServer(); const page = usePlaylistDetailStore(); - const filters: Partial = useMemo(() => { + const filters: PlaylistSongListQueryClientSide = useMemo(() => { return { sortBy: page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID, sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC, @@ -88,20 +88,6 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED; - const iSClientSide = server?.type === ServerType.SUBSONIC; - - const checkPlaylistList = usePlaylistSongList({ - options: { - enabled: !iSClientSide, - }, - query: { - id: playlistId, - limit: 1, - startIndex: 0, - }, - serverId: server?.id, - }); - const columnDefs: ColDef[] = useMemo( () => getColumnDefs(page.table.columns, false, 'generic'), [page.table.columns], @@ -109,51 +95,9 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai const onGridReady = useCallback( (params: GridReadyEvent) => { - if (!iSClientSide) { - const dataSource: IDatasource = { - getRows: async (params) => { - const limit = params.endRow - params.startRow; - const startIndex = params.startRow; - - const query: PlaylistSongListQuery = { - id: playlistId, - limit, - startIndex, - ...filters, - }; - - const queryKey = queryKeys.playlists.songList( - server?.id || '', - playlistId, - query, - ); - - if (!server) return; - - const songsRes = await queryClient.fetchQuery( - queryKey, - async ({ signal }) => - api.controller.getPlaylistSongList({ - apiClientProps: { - server, - signal, - }, - query, - }), - ); - - params.successCallback( - songsRes?.items || [], - songsRes?.totalRecordCount || 0, - ); - }, - rowCount: undefined, - }; - params.api.setDatasource(dataSource); - } params.api?.ensureIndexVisible(pagination.scrollOffset, 'top'); }, - [filters, iSClientSide, pagination.scrollOffset, playlistId, queryClient, server], + [pagination.scrollOffset], ); const handleDragEnd = useCallback( @@ -175,12 +119,32 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai }, }); - setTimeout(() => { - queryClient.invalidateQueries({ - queryKey: queryKeys.playlists.songList(server?.id || '', playlistId), - }); - e.api.refreshInfiniteCache(); - }, 200); + queryClient.setQueryData( + queryKeys.playlists.songList(server?.id || '', playlistId), + (previous) => { + if (previous?.items) { + const from = e.node.rowIndex!; + const to = e.overIndex; + + const item = previous.items[from]; + const remaining = previous.items.toSpliced(from, 1); + remaining.splice(to, 0, item); + + return { + error: previous.error, + items: remaining, + startIndex: previous.startIndex, + totalRecordCount: previous.totalRecordCount, + }; + } + + return previous; + }, + ); + + // Nodes have to be redrawn, otherwise the row indexes will be wrong + // Maybe it's possible to only redraw necessary rows to not be as expensive? + tableRef.current?.api.redrawRows(); } catch (error) { toast.error({ message: (error as Error).message, @@ -189,7 +153,7 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai } } }, - [playlistId, queryClient, server], + [playlistId, queryClient, server, tableRef], ); const handleGridSizeChange = () => { @@ -286,7 +250,9 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai const { rowClassRules } = useCurrentSongRowStyles({ tableRef }); const canDrag = - filters.sortBy === SongListSort.ID && !detailQuery?.data?.rules && !iSClientSide; + filters.sortBy === SongListSort.ID && + !detailQuery?.data?.rules && + server?.type !== ServerType.SUBSONIC; return ( <> @@ -303,9 +269,6 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai status, }} getRowId={(data) => data.data.uniqueId} - infiniteInitialRowCount={ - iSClientSide ? undefined : checkPlaylistList.data?.totalRecordCount || 100 - } // https://github.com/ag-grid/ag-grid/issues/5284 // Key is used to force remount of table when display, rowHeight, or server changes key={`table-${page.display}-${page.table.rowHeight}-${server?.id}`} @@ -326,7 +289,7 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai rowData={songs} rowDragEntireRow={canDrag} rowHeight={page.table.rowHeight || 40} - rowModelType={iSClientSide ? 'clientSide' : 'infinite'} + rowModelType="clientSide" shouldUpdateSong /> diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-header-filters.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-header-filters.tsx index 8b1c7e0f..ec7ed02c 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-header-filters.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-header-filters.tsx @@ -1,6 +1,5 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; -import { IDatasource } from '@ag-grid-community/core'; import { closeAllModals, openModal } from '@mantine/modals'; import { useQueryClient } from '@tanstack/react-query'; import debounce from 'lodash/debounce'; @@ -9,7 +8,6 @@ import { useTranslation } from 'react-i18next'; import { useNavigate, useParams } from 'react-router'; import i18n from '/@/i18n/i18n'; -import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; import { usePlayQueueAdd } from '/@/renderer/features/player'; @@ -23,7 +21,6 @@ import { useContainerQuery } from '/@/renderer/hooks'; import { AppRoute } from '/@/renderer/router/routes'; import { PersistedTableColumn, - SongListFilter, useCurrentServer, usePlaylistDetailStore, useSetPlaylistDetailFilters, @@ -42,7 +39,7 @@ import { Text } from '/@/shared/components/text/text'; import { toast } from '/@/shared/components/toast/toast'; import { LibraryItem, - PlaylistSongListQuery, + PlaylistSongListQueryClientSide, ServerType, SongListSort, SortOrder, @@ -155,7 +152,7 @@ const FILTERS = { }, { defaultOrder: SortOrder.ASC, - name: i18n.t('filter.playCount', { postProcess: 'titleCase' }), + name: i18n.t('filter.genre', { postProcess: 'titleCase' }), value: SongListSort.GENRE, }, { @@ -240,11 +237,6 @@ const FILTERS = { name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }), value: SongListSort.RECENTLY_ADDED, }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }), - value: SongListSort.RECENTLY_PLAYED, - }, { defaultOrder: SortOrder.DESC, name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }), @@ -270,7 +262,7 @@ export const PlaylistDetailSongListHeaderFilters = ({ const setPage = useSetPlaylistStore(); const setFilter = useSetPlaylistDetailFilters(); const page = usePlaylistDetailStore(); - const filters: Partial = { + const filters: Partial = { sortBy: page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID, sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC, }; @@ -297,68 +289,18 @@ export const PlaylistDetailSongListHeaderFilters = ({ const debouncedHandleItemSize = debounce(handleItemSize, 20); - const handleFilterChange = useCallback( - async (filters: SongListFilter) => { - if (server?.type !== ServerType.SUBSONIC) { - const dataSource: IDatasource = { - getRows: async (params) => { - const limit = params.endRow - params.startRow; - const startIndex = params.startRow; + const handleFilterChange = useCallback(async () => { + tableRef.current?.api.redrawRows(); + tableRef.current?.api.ensureIndexVisible(0, 'top'); - const queryKey = queryKeys.playlists.songList( - server?.id || '', - playlistId, - { - id: playlistId, - limit, - startIndex, - ...filters, - }, - ); - - const songsRes = await queryClient.fetchQuery( - queryKey, - async ({ signal }) => - api.controller.getPlaylistSongList({ - apiClientProps: { - server, - signal, - }, - query: { - id: playlistId, - limit, - startIndex, - ...filters, - }, - }), - { cacheTime: 1000 * 60 * 1 }, - ); - - params.successCallback( - songsRes?.items || [], - songsRes?.totalRecordCount || 0, - ); - }, - rowCount: undefined, - }; - tableRef.current?.api.setDatasource(dataSource); - tableRef.current?.api.purgeInfiniteCache(); - tableRef.current?.api.ensureIndexVisible(0, 'top'); - } else { - tableRef.current?.api.redrawRows(); - tableRef.current?.api.ensureIndexVisible(0, 'top'); - } - - if (page.display === ListDisplayType.TABLE_PAGINATED) { - setPagination({ data: { currentPage: 0 } }); - } - }, - [tableRef, page.display, server, playlistId, queryClient, setPagination], - ); + if (page.display === ListDisplayType.TABLE_PAGINATED) { + setPagination({ data: { currentPage: 0 } }); + } + }, [tableRef, page.display, setPagination]); const handleRefresh = () => { - queryClient.invalidateQueries(queryKeys.albums.list(server?.id || '')); - handleFilterChange({ ...page?.table.id[playlistId].filter, ...filters }); + queryClient.invalidateQueries(queryKeys.playlists.songList(server?.id || '', playlistId)); + handleFilterChange(); }; const handleSetSortBy = useCallback( @@ -369,20 +311,20 @@ export const PlaylistDetailSongListHeaderFilters = ({ (f) => f.value === e.currentTarget.value, )?.defaultOrder; - const updatedFilters = setFilter(playlistId, { + setFilter(playlistId, { sortBy: e.currentTarget.value as SongListSort, sortOrder: sortOrder || SortOrder.ASC, }); - handleFilterChange(updatedFilters); + handleFilterChange(); }, [handleFilterChange, playlistId, server?.type, setFilter], ); const handleToggleSortOrder = useCallback(() => { const newSortOrder = filters.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; - const updatedFilters = setFilter(playlistId, { sortOrder: newSortOrder }); - handleFilterChange(updatedFilters); + setFilter(playlistId, { sortOrder: newSortOrder }); + handleFilterChange(); }, [filters.sortOrder, handleFilterChange, playlistId, setFilter]); const handleSetViewType = useCallback( @@ -432,6 +374,7 @@ export const PlaylistDetailSongListHeaderFilters = ({ handlePlayQueueAdd?.({ byItemType: { id: [playlistId], type: LibraryItem.PLAYLIST }, playType, + query: filters, }); }; diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-header.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-header.tsx index 90fded26..384f9265 100644 --- a/src/renderer/features/playlists/components/playlist-detail-song-list-header.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-header.tsx @@ -9,12 +9,17 @@ import { usePlayQueueAdd } from '/@/renderer/features/player'; import { PlaylistDetailSongListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header-filters'; import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query'; import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared'; -import { useCurrentServer } from '/@/renderer/store'; +import { useCurrentServer, usePlaylistDetailStore } from '/@/renderer/store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { Badge } from '/@/shared/components/badge/badge'; import { SpinnerIcon } from '/@/shared/components/spinner/spinner'; import { Stack } from '/@/shared/components/stack/stack'; -import { LibraryItem } from '/@/shared/types/domain-types'; +import { + LibraryItem, + PlaylistSongListQueryClientSide, + SongListSort, + SortOrder, +} from '/@/shared/types/domain-types'; import { Play } from '/@/shared/types/types'; interface PlaylistDetailHeaderProps { @@ -33,11 +38,17 @@ export const PlaylistDetailSongListHeader = ({ const server = useCurrentServer(); const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); const handlePlayQueueAdd = usePlayQueueAdd(); + const page = usePlaylistDetailStore(); + const filters: Partial = { + sortBy: page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID, + sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC, + }; const handlePlay = async (playType: Play) => { handlePlayQueueAdd?.({ byItemType: { id: [playlistId], type: LibraryItem.PLAYLIST }, playType, + query: filters, }); }; diff --git a/src/renderer/features/playlists/mutations/add-to-playlist-mutation.ts b/src/renderer/features/playlists/mutations/add-to-playlist-mutation.ts index b699fdb7..69978df6 100644 --- a/src/renderer/features/playlists/mutations/add-to-playlist-mutation.ts +++ b/src/renderer/features/playlists/mutations/add-to-playlist-mutation.ts @@ -29,9 +29,6 @@ export const useAddToPlaylist = (args: MutationHookArgs) => { queryClient.invalidateQueries(queryKeys.playlists.list(serverId), { exact: false }); queryClient.invalidateQueries(queryKeys.playlists.detail(serverId, variables.query.id)); - queryClient.invalidateQueries( - queryKeys.playlists.detailSongList(serverId, variables.query.id), - ); queryClient.invalidateQueries( queryKeys.playlists.songList(serverId, variables.query.id), ); diff --git a/src/renderer/features/playlists/mutations/remove-from-playlist-mutation.ts b/src/renderer/features/playlists/mutations/remove-from-playlist-mutation.ts index fdd57da6..0fada987 100644 --- a/src/renderer/features/playlists/mutations/remove-from-playlist-mutation.ts +++ b/src/renderer/features/playlists/mutations/remove-from-playlist-mutation.ts @@ -29,7 +29,7 @@ export const useRemoveFromPlaylist = (options?: MutationOptions) => { queryClient.invalidateQueries(queryKeys.playlists.list(serverId), { exact: false }); queryClient.invalidateQueries(queryKeys.playlists.detail(serverId, variables.query.id)); queryClient.invalidateQueries( - queryKeys.playlists.detailSongList(serverId, variables.query.id), + queryKeys.playlists.songList(serverId, variables.query.id), ); }, ...options, diff --git a/src/renderer/features/playlists/queries/playlist-song-list-query.ts b/src/renderer/features/playlists/queries/playlist-song-list-query.ts index 15585b28..edc26988 100644 --- a/src/renderer/features/playlists/queries/playlist-song-list-query.ts +++ b/src/renderer/features/playlists/queries/playlist-song-list-query.ts @@ -20,7 +20,7 @@ export const usePlaylistSongList = (args: QueryHookArgs) query, }); }, - queryKey: queryKeys.playlists.songList(server?.id || '', query.id, query), + queryKey: queryKeys.playlists.songList(server?.id || '', query.id), ...options, }); }; diff --git a/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx b/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx index 09a24346..3a124fdb 100644 --- a/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx +++ b/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx @@ -2,7 +2,7 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li import { closeAllModals, openModal } from '@mantine/modals'; import { motion } from 'motion/react'; -import { useRef, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { generatePath, useNavigate, useParams } from 'react-router'; @@ -22,12 +22,7 @@ import { Box } from '/@/shared/components/box/box'; import { Group } from '/@/shared/components/group/group'; import { Text } from '/@/shared/components/text/text'; import { toast } from '/@/shared/components/toast/toast'; -import { - PlaylistSongListQuery, - ServerType, - SongListSort, - SortOrder, -} from '/@/shared/types/domain-types'; +import { ServerType, SongListSort, SortOrder, sortSongList } from '/@/shared/types/domain-types'; const PlaylistDetailSongListRoute = () => { const { t } = useTranslation(); @@ -148,22 +143,25 @@ const PlaylistDetailSongListRoute = () => { }; const page = usePlaylistDetailStore(); - const filters: Partial = { - sortBy: page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID, - sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC, - }; - const itemCountCheck = usePlaylistSongList({ + const playlistSongs = usePlaylistSongList({ query: { id: playlistId, - limit: 1, - startIndex: 0, - ...filters, }, serverId: server?.id, }); - const itemCount = itemCountCheck.data?.totalRecordCount || itemCountCheck.data?.items.length; + const itemCount = playlistSongs.data?.totalRecordCount ?? undefined; + + const filterSortedSongs = useMemo(() => { + if (playlistSongs.data?.items) { + const sortBy = page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID; + const sortOrder = page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC; + return sortSongList(playlistSongs.data?.items, sortBy, sortOrder); + } else { + return []; + } + }, [playlistSongs.data?.items, page?.table.id, playlistId]); return ( @@ -203,12 +201,7 @@ const PlaylistDetailSongListRoute = () => { )} - + ); }; diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index 137d72e7..6df047f6 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -988,10 +988,11 @@ export type PlaylistSongListArgs = BaseEndpointArgs & { query: PlaylistSongListQ export type PlaylistSongListQuery = { id: string; - limit?: number; +}; + +export type PlaylistSongListQueryClientSide = { sortBy?: SongListSort; sortOrder?: SortOrder; - startIndex: number; }; // Playlist Songs @@ -1400,7 +1401,7 @@ export const sortSongList = (songs: QueueSong[], sortBy: SongListSort, sortOrder case SongListSort.ALBUM_ARTIST: results = orderBy( results, - ['albumArtist', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'], + [(v) => v.albumArtists[0]?.name.toLowerCase(), 'discNumber', 'trackNumber'], [order, order, 'asc', 'asc'], ); break; @@ -1408,11 +1409,23 @@ export const sortSongList = (songs: QueueSong[], sortBy: SongListSort, sortOrder case SongListSort.ARTIST: results = orderBy( results, - ['artist', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'], + [(v) => v.artistName?.toLowerCase(), 'discNumber', 'trackNumber'], [order, order, 'asc', 'asc'], ); break; + case SongListSort.BPM: + results = orderBy(results, ['bpm'], [order]); + break; + + case SongListSort.CHANNELS: + results = orderBy(results, ['channels'], [order]); + break; + + case SongListSort.COMMENT: + results = orderBy(results, ['comment'], [order]); + break; + case SongListSort.DURATION: results = orderBy(results, ['duration'], [order]); break; @@ -1425,7 +1438,7 @@ export const sortSongList = (songs: QueueSong[], sortBy: SongListSort, sortOrder results = orderBy( results, [ - (v) => v.genres?.[0].name.toLowerCase(), + (v) => v.genres?.[0]?.name.toLowerCase(), (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber', @@ -1457,13 +1470,21 @@ export const sortSongList = (songs: QueueSong[], sortBy: SongListSort, sortOrder break; case SongListSort.RECENTLY_ADDED: - results = orderBy(results, ['created'], [order]); + results = orderBy(results, ['createdAt'], [order]); + break; + + case SongListSort.RECENTLY_PLAYED: + results = orderBy(results, ['lastPlayedAt'], [order]); + break; + + case SongListSort.RELEASE_DATE: + results = orderBy(results, ['releaseDate'], [order]); break; case SongListSort.YEAR: results = orderBy( results, - ['year', (v) => v.album?.toLowerCase(), 'discNumber', 'track'], + ['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'track'], [order, 'asc', 'asc', 'asc'], ); break;