diff --git a/src/renderer/components/index.ts b/src/renderer/components/index.ts index 2d3f27b0..61943342 100644 --- a/src/renderer/components/index.ts +++ b/src/renderer/components/index.ts @@ -27,8 +27,6 @@ export * from './text'; export * from './text-title'; export * from './toast'; export * from './tooltip'; -export * from './virtual-grid'; -export * from './virtual-table'; export * from './motion'; export * from './context-menu'; export * from './query-builder'; diff --git a/src/renderer/components/virtual-table/cells/favorite-cell.tsx b/src/renderer/components/virtual-table/cells/favorite-cell.tsx index 5bbacada..94fa4edf 100644 --- a/src/renderer/components/virtual-table/cells/favorite-cell.tsx +++ b/src/renderer/components/virtual-table/cells/favorite-cell.tsx @@ -1,50 +1,13 @@ +/* eslint-disable import/no-cycle */ import type { ICellRendererParams } from '@ag-grid-community/core'; import { RiHeartFill, RiHeartLine } from 'react-icons/ri'; import { Button } from '/@/renderer/components/button'; import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell'; -import { useMutation } from '@tanstack/react-query'; -import { HTTPError } from 'ky'; -import { api } from '/@/renderer/api'; -import { RawFavoriteResponse, FavoriteArgs, LibraryItem } from '/@/renderer/api/types'; -import { useCurrentServer, useSetAlbumListItemDataById } from '/@/renderer/store'; - -const useCreateFavorite = () => { - const server = useCurrentServer(); - const setAlbumListData = useSetAlbumListItemDataById(); - - return useMutation, null>({ - mutationFn: (args) => api.controller.createFavorite({ ...args, server }), - onSuccess: (_data, variables) => { - for (const id of variables.query.id) { - // Set the userFavorite property to true for the album in the album list data store - if (variables.query.type === LibraryItem.ALBUM) { - setAlbumListData(id, { userFavorite: true }); - } - } - }, - }); -}; - -const useDeleteFavorite = () => { - const server = useCurrentServer(); - const setAlbumListData = useSetAlbumListItemDataById(); - - return useMutation, null>({ - mutationFn: (args) => api.controller.deleteFavorite({ ...args, server }), - onSuccess: (_data, variables) => { - for (const id of variables.query.id) { - // Set the userFavorite property to false for the album in the album list data store - if (variables.query.type === LibraryItem.ALBUM) { - setAlbumListData(id, { userFavorite: false }); - } - } - }, - }); -}; +import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared'; export const FavoriteCell = ({ value, data, node }: ICellRendererParams) => { - const createMutation = useCreateFavorite(); - const deleteMutation = useDeleteFavorite(); + const createMutation = useCreateFavorite({}); + const deleteMutation = useDeleteFavorite({}); const handleToggleFavorite = () => { const newFavoriteValue = !value; diff --git a/src/renderer/components/virtual-table/cells/rating-cell.tsx b/src/renderer/components/virtual-table/cells/rating-cell.tsx index 74f68a44..a6695540 100644 --- a/src/renderer/components/virtual-table/cells/rating-cell.tsx +++ b/src/renderer/components/virtual-table/cells/rating-cell.tsx @@ -1,22 +1,23 @@ +/* eslint-disable import/no-cycle */ import { MouseEvent } from 'react'; import type { ICellRendererParams } from '@ag-grid-community/core'; import { Rating } from '/@/renderer/components/rating'; import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell'; -import { useUpdateRating } from '/@/renderer/components/virtual-table/hooks/use-rating'; +import { useSetRating } from '/@/renderer/features/shared'; export const RatingCell = ({ value, node }: ICellRendererParams) => { - const updateRatingMutation = useUpdateRating(); + const updateRatingMutation = useSetRating({}); const handleUpdateRating = (rating: number) => { if (!value) return; updateRatingMutation.mutate( { - _serverId: value?.serverId, query: { item: [value], rating, }, + serverId: value?.serverId, }, { onSuccess: () => { @@ -31,11 +32,11 @@ export const RatingCell = ({ value, node }: ICellRendererParams) => { e.stopPropagation(); updateRatingMutation.mutate( { - _serverId: value?.serverId, query: { item: [value], rating: 0, }, + serverId: value?.serverId, }, { onSuccess: () => { diff --git a/src/renderer/components/virtual-table/index.tsx b/src/renderer/components/virtual-table/index.tsx index 76e83ce6..046cab0b 100644 --- a/src/renderer/components/virtual-table/index.tsx +++ b/src/renderer/components/virtual-table/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable import/no-cycle */ import { Ref, forwardRef, useRef, useEffect, useCallback, useMemo } from 'react'; import type { ICellRendererParams, @@ -28,8 +29,8 @@ import { GenericTableHeader } from '/@/renderer/components/virtual-table/headers import { AppRoute } from '/@/renderer/router/routes'; import { PersistedTableColumn } from '/@/renderer/store/settings.store'; import { TableColumn } from '/@/renderer/types'; -import { RatingCell } from '/@/renderer/components/virtual-table/cells/rating-cell'; import { FavoriteCell } from '/@/renderer/components/virtual-table/cells/favorite-cell'; +import { RatingCell } from '/@/renderer/components/virtual-table/cells/rating-cell'; export * from './table-config-dropdown'; export * from './table-pagination'; diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index 79eb16d3..71319cbb 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -1,13 +1,5 @@ import { MutableRefObject, useCallback, useMemo } from 'react'; -import { - Button, - getColumnDefs, - GridCarousel, - Text, - TextTitle, - useFixedTableHeader, - VirtualTable, -} from '/@/renderer/components'; +import { Button, GridCarousel, Text, TextTitle } from '/@/renderer/components'; import { ColDef, RowDoubleClickedEvent, RowHeightParams, RowNode } from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { Box, Group, Stack } from '@mantine/core'; @@ -33,6 +25,12 @@ import { PlayButton, useCreateFavorite, useDeleteFavorite } from '/@/renderer/fe import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query'; import { AlbumListSort, LibraryItem, QueueSong, SortOrder } from '/@/renderer/api/types'; import { usePlayQueueAdd } from '/@/renderer/features/player'; +import { useCurrentServer } from '/@/renderer/store'; +import { + getColumnDefs, + useFixedTableHeader, + VirtualTable, +} from '/@/renderer/components/virtual-table'; const isFullWidthRow = (node: RowNode) => { return node.id?.includes('disc-'); @@ -60,7 +58,8 @@ interface AlbumDetailContentProps { export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => { const { albumId } = useParams() as { albumId: string }; - const detailQuery = useAlbumDetail({ id: albumId }); + const server = useCurrentServer(); + const detailQuery = useAlbumDetail({ query: { id: albumId }, serverId: server?.id }); const cq = useContainerQuery(); const handlePlayQueueAdd = usePlayQueueAdd(); @@ -165,26 +164,29 @@ export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => { const itemsPerPage = cq.isXl ? 9 : cq.isLg ? 7 : cq.isMd ? 5 : cq.isSm ? 4 : 3; - const artistQuery = useAlbumList( - { - jfParams: { - albumArtistIds: detailQuery?.data?.albumArtists[0]?.id, - }, - limit: itemsPerPage, - ndParams: { - artist_id: detailQuery?.data?.albumArtists[0]?.id, - }, - sortBy: AlbumListSort.YEAR, - sortOrder: SortOrder.DESC, - startIndex: pagination.artist * itemsPerPage, - }, - { + const artistQuery = useAlbumList({ + options: { cacheTime: 1000 * 60, enabled: detailQuery?.data?.albumArtists[0]?.id !== undefined, keepPreviousData: true, staleTime: 1000 * 60, }, - ); + query: { + _custom: { + jellyfin: { + albumArtistIds: detailQuery?.data?.albumArtists[0]?.id, + }, + navidrome: { + artist_id: detailQuery?.data?.albumArtists[0]?.id, + }, + }, + limit: itemsPerPage, + sortBy: AlbumListSort.YEAR, + sortOrder: SortOrder.DESC, + startIndex: pagination.artist * itemsPerPage, + }, + serverId: server?.id, + }); const carousels = [ { @@ -227,8 +229,8 @@ export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => { }); }; - const createFavoriteMutation = useCreateFavorite(); - const deleteFavoriteMutation = useDeleteFavorite(); + const createFavoriteMutation = useCreateFavorite({}); + const deleteFavoriteMutation = useDeleteFavorite({}); const handleFavorite = () => { if (!detailQuery?.data) return; diff --git a/src/renderer/features/albums/components/album-detail-header.tsx b/src/renderer/features/albums/components/album-detail-header.tsx index 357bec6f..46a0f74a 100644 --- a/src/renderer/features/albums/components/album-detail-header.tsx +++ b/src/renderer/features/albums/components/album-detail-header.tsx @@ -5,9 +5,10 @@ import { Link } from 'react-router-dom'; import { LibraryItem, ServerType } from '/@/renderer/api/types'; import { Button, Rating, Text } from '/@/renderer/components'; import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query'; -import { LibraryHeader, useUpdateRating } from '/@/renderer/features/shared'; +import { LibraryHeader, useSetRating } from '/@/renderer/features/shared'; import { useContainerQuery } from '/@/renderer/hooks'; import { AppRoute } from '/@/renderer/router/routes'; +import { useCurrentServer } from '/@/renderer/store'; import { formatDurationString } from '/@/renderer/utils'; interface AlbumDetailHeaderProps { @@ -17,7 +18,8 @@ interface AlbumDetailHeaderProps { export const AlbumDetailHeader = forwardRef( ({ background }: AlbumDetailHeaderProps, ref: Ref) => { const { albumId } = useParams() as { albumId: string }; - const detailQuery = useAlbumDetail({ id: albumId }); + const server = useCurrentServer(); + const detailQuery = useAlbumDetail({ query: { id: albumId }, serverId: server?.id }); const cq = useContainerQuery(); const metadataItems = [ @@ -38,17 +40,17 @@ export const AlbumDetailHeader = forwardRef( }, ]; - const updateRatingMutation = useUpdateRating(); + const updateRatingMutation = useSetRating({}); const handleUpdateRating = (rating: number) => { if (!detailQuery?.data) return; updateRatingMutation.mutate({ - _serverId: detailQuery?.data.serverId, query: { item: [detailQuery.data], rating, }, + serverId: detailQuery.data.serverId, }); }; @@ -56,11 +58,11 @@ export const AlbumDetailHeader = forwardRef( if (!detailQuery?.data || !detailQuery?.data.userRating) return; updateRatingMutation.mutate({ - _serverId: detailQuery.data.serverId, query: { item: [detailQuery.data], rating: 0, }, + serverId: detailQuery.data.serverId, }); }; diff --git a/src/renderer/features/albums/components/album-list-content.tsx b/src/renderer/features/albums/components/album-list-content.tsx index 4e092d1b..437c8299 100644 --- a/src/renderer/features/albums/components/album-list-content.tsx +++ b/src/renderer/features/albums/components/album-list-content.tsx @@ -1,12 +1,4 @@ -import { - ALBUM_CARD_ROWS, - getColumnDefs, - TablePagination, - VirtualGridAutoSizerContainer, - VirtualInfiniteGrid, - VirtualInfiniteGridRef, - VirtualTable, -} from '/@/renderer/components'; +import { ALBUM_CARD_ROWS } from '/@/renderer/components'; import { AppRoute } from '/@/renderer/router/routes'; import { ListDisplayType, CardRow } from '/@/renderer/types'; import AutoSizer from 'react-virtualized-auto-sizer'; @@ -40,6 +32,12 @@ import { generatePath, useNavigate } from 'react-router'; import { usePlayQueueAdd } from '/@/renderer/features/player'; import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared'; import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; +import { + VirtualInfiniteGridRef, + VirtualGridAutoSizerContainer, + VirtualInfiniteGrid, +} from '/@/renderer/components/virtual-grid'; +import { getColumnDefs, VirtualTable, TablePagination } from '/@/renderer/components/virtual-table'; interface AlbumListContentProps { gridRef: MutableRefObject; @@ -71,29 +69,36 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont limit, startIndex, ...filter, - jfParams: { - ...filter.jfParams, - }, - ndParams: { - ...filter.ndParams, + _custom: { + jellyfin: { + ...filter._custom?.jellyfin, + }, + navidrome: { + ...filter._custom?.navidrome, + }, }, }; const queryKey = queryKeys.albums.list(server?.id || '', query); + if (!server) { + return params.failCallback(); + } + const albumsRes = await queryClient.fetchQuery( queryKey, async ({ signal }) => api.controller.getAlbumList({ + apiClientProps: { + server, + signal, + }, query, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); - const albums = api.normalize.albumList(albumsRes, server); - params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || 0); + return params.successCallback(albumsRes?.items || [], albumsRes?.totalRecordCount || 0); }, rowCount: undefined, }; @@ -165,15 +170,21 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont const fetch = useCallback( async ({ skip, take }: { skip: number; take: number }) => { + if (!server) { + return []; + } + const query: AlbumListQuery = { limit: take, startIndex: skip, ...filter, - jfParams: { - ...filter.jfParams, - }, - ndParams: { - ...filter.ndParams, + _custom: { + jellyfin: { + ...filter._custom?.jellyfin, + }, + navidrome: { + ...filter._custom?.navidrome, + }, }, }; @@ -181,13 +192,15 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) => controller.getAlbumList({ + apiClientProps: { + server, + signal, + }, query, - server, - signal, }), ); - return api.normalize.albumList(albums, server); + return albums; }, [filter, queryClient, server], ); @@ -268,8 +281,8 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id })); }; - const createFavoriteMutation = useCreateFavorite(); - const deleteFavoriteMutation = useDeleteFavorite(); + const createFavoriteMutation = useCreateFavorite({}); + const deleteFavoriteMutation = useDeleteFavorite({}); const handleFavorite = (options: { id: string[]; diff --git a/src/renderer/features/albums/components/album-list-header-filters.tsx b/src/renderer/features/albums/components/album-list-header-filters.tsx index d9b47f84..25ebf5e7 100644 --- a/src/renderer/features/albums/components/album-list-header-filters.tsx +++ b/src/renderer/features/albums/components/album-list-header-filters.tsx @@ -20,16 +20,7 @@ import { import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; import { AlbumListQuery, AlbumListSort, LibraryItem, SortOrder } from '/@/renderer/api/types'; -import { - ALBUM_TABLE_COLUMNS, - Button, - DropdownMenu, - MultiSelect, - Slider, - Switch, - Text, - VirtualInfiniteGridRef, -} from '/@/renderer/components'; +import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components'; import { useContainerQuery } from '/@/renderer/hooks'; import { AlbumListFilter, @@ -43,6 +34,8 @@ import { usePlayQueueAdd } from '/@/renderer/features/player'; import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters'; import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters'; import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; +import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; +import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; const FILTERS = { jellyfin: [ @@ -100,7 +93,7 @@ export const AlbumListHeaderFilters = ({ const { display, filter, table, grid } = useAlbumListStore({ id, key: pageKey }); const cq = useContainerQuery(); - const musicFoldersQuery = useMusicFolders(); + const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id }); const sortByLabel = (server?.type && @@ -115,13 +108,15 @@ export const AlbumListHeaderFilters = ({ limit: take, startIndex: skip, ...filters, - jfParams: { - ...filters.jfParams, - ...customFilters?.jfParams, - }, - ndParams: { - ...filters.ndParams, - ...customFilters?.ndParams, + _custom: { + jellyfin: { + ...filters._custom?.jellyfin, + ...customFilters?._custom?.jellyfin, + }, + navidrome: { + ...filters._custom?.navidrome, + ...customFilters?._custom?.navidrome, + }, }, ...customFilters, }; @@ -132,14 +127,16 @@ export const AlbumListHeaderFilters = ({ queryKey, async ({ signal }) => api.controller.getAlbumList({ + apiClientProps: { + server, + signal, + }, query, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); - return api.normalize.albumList(albums, server); + return albums; }, [customFilters, queryClient, server], ); @@ -157,13 +154,15 @@ export const AlbumListHeaderFilters = ({ startIndex, ...filters, ...customFilters, - jfParams: { - ...filters.jfParams, - ...customFilters?.jfParams, - }, - ndParams: { - ...filters.ndParams, - ...customFilters?.ndParams, + _custom: { + jellyfin: { + ...filters._custom?.jellyfin, + ...customFilters?._custom?.jellyfin, + }, + navidrome: { + ...filters._custom?.navidrome, + ...customFilters?._custom?.navidrome, + }, }, }; @@ -173,15 +172,16 @@ export const AlbumListHeaderFilters = ({ queryKey, async ({ signal }) => api.controller.getAlbumList({ + apiClientProps: { + server, + signal, + }, query, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); - const albums = api.normalize.albumList(albumsRes, server); - params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || 0); + return params.successCallback(albumsRes?.items || [], albumsRes?.totalRecordCount || 0); }, rowCount: undefined, }; @@ -218,6 +218,7 @@ export const AlbumListHeaderFilters = ({ handleFilterChange={handleFilterChange} id={id} pageKey={pageKey} + serverId={server?.id} /> ) : ( )} @@ -293,30 +295,32 @@ export const AlbumListHeaderFilters = ({ const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlay = async (play: Play) => { - if (!itemCount || itemCount === 0) return; + if (!itemCount || itemCount === 0 || !server) return; const query = { startIndex: 0, ...filter, ...customFilters, - jfParams: { - ...filter.jfParams, - ...customFilters?.jfParams, - }, - ndParams: { - ...filter.ndParams, - ...customFilters?.ndParams, + _custom: { + jellyfin: { + ...filter._custom?.jellyfin, + ...customFilters?._custom?.jellyfin, + }, + navidrome: { + ...filter._custom?.navidrome, + ...customFilters?._custom?.navidrome, + }, }, }; const queryKey = queryKeys.albums.list(server?.id || '', query); const albumListRes = await queryClient.fetchQuery({ - queryFn: ({ signal }) => api.controller.getAlbumList({ query, server, signal }), + queryFn: ({ signal }) => + api.controller.getAlbumList({ apiClientProps: { server, signal }, query }), queryKey, }); - const albumIds = - api.normalize.albumList(albumListRes, server).items?.map((item) => item.id) || []; + const albumIds = albumListRes?.items?.map((a) => a.id) || []; handlePlayQueueAdd?.({ byItemType: { @@ -382,16 +386,16 @@ export const AlbumListHeaderFilters = ({ const isFilterApplied = useMemo(() => { const isNavidromeFilterApplied = server?.type === ServerType.NAVIDROME && - filter.ndParams && - Object.values(filter.ndParams).some((value) => value !== undefined); + filter?._custom.navidrome && + Object.values(filter._custom.navidrome).some((value) => value !== undefined); const isJellyfinFilterApplied = server?.type === ServerType.JELLYFIN && - filter.jfParams && - Object.values(filter.jfParams).some((value) => value !== undefined); + filter?._custom.jellyfin && + Object.values(filter._custom.jellyfin).some((value) => value !== undefined); return isNavidromeFilterApplied || isJellyfinFilterApplied; - }, [filter.jfParams, filter.ndParams, server?.type]); + }, [filter._custom.jellyfin, filter._custom.navidrome, server?.type]); return ( @@ -456,7 +460,7 @@ export const AlbumListHeaderFilters = ({ - {musicFoldersQuery.data?.map((folder) => ( + {musicFoldersQuery.data?.items.map((folder) => ( ; @@ -54,15 +55,17 @@ export const AlbumListHeader = ({ limit: take, startIndex: skip, ...filters, - jfParams: { - ...filters.jfParams, - ...customFilters?.jfParams, - }, - ndParams: { - ...filters.ndParams, - ...customFilters?.ndParams, - }, ...customFilters, + _custom: { + jellyfin: { + ...filters._custom?.jellyfin, + ...customFilters?._custom?.jellyfin, + }, + navidrome: { + ...filters._custom?.navidrome, + ...customFilters?._custom?.navidrome, + }, + }, }; const queryKey = queryKeys.albums.list(server?.id || '', query); @@ -71,14 +74,16 @@ export const AlbumListHeader = ({ queryKey, async ({ signal }) => controller.getAlbumList({ + apiClientProps: { + server, + signal, + }, query, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); - return api.normalize.albumList(albums, server); + return albums; }, [customFilters, queryClient, server], ); @@ -96,13 +101,15 @@ export const AlbumListHeader = ({ startIndex, ...filters, ...customFilters, - jfParams: { - ...filters.jfParams, - ...customFilters?.jfParams, - }, - ndParams: { - ...filters.ndParams, - ...customFilters?.ndParams, + _custom: { + jellyfin: { + ...filters._custom?.jellyfin, + ...customFilters?._custom?.jellyfin, + }, + navidrome: { + ...filters._custom?.navidrome, + ...customFilters?._custom?.navidrome, + }, }, }; @@ -112,15 +119,16 @@ export const AlbumListHeader = ({ queryKey, async ({ signal }) => api.controller.getAlbumList({ + apiClientProps: { + server, + signal, + }, query, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); - const albums = api.normalize.albumList(albumsRes, server); - params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || 0); + params.successCallback(albumsRes?.items || [], albumsRes?.totalRecordCount || 0); }, rowCount: undefined, }; @@ -164,24 +172,26 @@ export const AlbumListHeader = ({ startIndex: 0, ...filter, ...customFilters, - jfParams: { - ...filter.jfParams, - ...customFilters?.jfParams, - }, - ndParams: { - ...filter.ndParams, - ...customFilters?.ndParams, + _custom: { + jellyfin: { + ...filter._custom?.jellyfin, + ...customFilters?._custom?.jellyfin, + }, + navidrome: { + ...filter._custom?.navidrome, + ...customFilters?._custom?.navidrome, + }, }, }; const queryKey = queryKeys.albums.list(server?.id || '', query); const albumListRes = await queryClient.fetchQuery({ - queryFn: ({ signal }) => api.controller.getAlbumList({ query, server, signal }), + queryFn: ({ signal }) => + api.controller.getAlbumList({ apiClientProps: { server, signal }, query }), queryKey, }); - const albumIds = - api.normalize.albumList(albumListRes, server).items?.map((item) => item.id) || []; + const albumIds = albumListRes?.items?.map((item) => item.id) || []; handlePlayQueueAdd?.({ byItemType: { diff --git a/src/renderer/features/albums/components/jellyfin-album-filters.tsx b/src/renderer/features/albums/components/jellyfin-album-filters.tsx index 8451521f..aacfdb5d 100644 --- a/src/renderer/features/albums/components/jellyfin-album-filters.tsx +++ b/src/renderer/features/albums/components/jellyfin-album-filters.tsx @@ -12,6 +12,7 @@ interface JellyfinAlbumFiltersProps { handleFilterChange: (filters: AlbumListFilter) => void; id?: string; pageKey: string; + serverId?: string; } export const JellyfinAlbumFilters = ({ @@ -19,24 +20,25 @@ export const JellyfinAlbumFilters = ({ handleFilterChange, pageKey, id, + serverId, }: JellyfinAlbumFiltersProps) => { const filter = useAlbumListFilter({ id, key: pageKey }); const { setFilter } = useListStoreActions(); // TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library - const genreListQuery = useGenreList(null); + const genreListQuery = useGenreList({ query: null, serverId }); const genreList = useMemo(() => { if (!genreListQuery?.data) return []; - return genreListQuery.data.map((genre) => ({ + return genreListQuery.data.items.map((genre) => ({ label: genre.name, value: genre.id, })); }, [genreListQuery.data]); const selectedGenres = useMemo(() => { - return filter.jfParams?.genreIds?.split(','); - }, [filter.jfParams?.genreIds]); + return filter._custom?.jellyfin?.genreIds?.split(','); + }, [filter._custom?.jellyfin?.genreIds]); const toggleFilters = [ { @@ -44,17 +46,19 @@ export const JellyfinAlbumFilters = ({ onChange: (e: ChangeEvent) => { const updatedFilters = setFilter({ data: { - jfParams: { - ...filter.jfParams, - includeItemTypes: 'Audio', - isFavorite: e.currentTarget.checked ? true : undefined, + _custom: { + ...filter._custom, + jellyfin: { + ...filter._custom?.jellyfin, + isFavorite: e.currentTarget.checked ? true : undefined, + }, }, }, key: pageKey, }) as AlbumListFilter; handleFilterChange(updatedFilters); }, - value: filter.jfParams?.isFavorite, + value: filter._custom?.jellyfin?.isFavorite, }, ]; @@ -62,9 +66,12 @@ export const JellyfinAlbumFilters = ({ if (typeof e === 'number' && (e < 1700 || e > 2300)) return; const updatedFilters = setFilter({ data: { - jfParams: { - ...filter.jfParams, - minYear: e === '' ? undefined : (e as number), + _custom: { + ...filter._custom, + jellyfin: { + ...filter._custom?.jellyfin, + minYear: e === '' ? undefined : (e as number), + }, }, }, key: pageKey, @@ -76,9 +83,12 @@ export const JellyfinAlbumFilters = ({ if (typeof e === 'number' && (e < 1700 || e > 2300)) return; const updatedFilters = setFilter({ data: { - jfParams: { - ...filter.jfParams, - maxYear: e === '' ? undefined : (e as number), + _custom: { + ...filter._custom, + jellyfin: { + ...filter._custom?.jellyfin, + maxYear: e === '' ? undefined : (e as number), + }, }, }, key: pageKey, @@ -90,9 +100,12 @@ export const JellyfinAlbumFilters = ({ const genreFilterString = e?.length ? e.join(',') : undefined; const updatedFilters = setFilter({ data: { - jfParams: { - ...filter.jfParams, - genreIds: genreFilterString, + _custom: { + ...filter._custom, + jellyfin: { + ...filter._custom?.jellyfin, + genreIds: genreFilterString, + }, }, }, key: pageKey, @@ -102,17 +115,18 @@ export const JellyfinAlbumFilters = ({ const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState(''); - const albumArtistListQuery = useAlbumArtistList( - { + const albumArtistListQuery = useAlbumArtistList({ + options: { + cacheTime: 1000 * 60 * 2, + staleTime: 1000 * 60 * 1, + }, + query: { sortBy: AlbumArtistListSort.NAME, sortOrder: SortOrder.ASC, startIndex: 0, }, - { - cacheTime: 1000 * 60 * 2, - staleTime: 1000 * 60 * 1, - }, - ); + serverId, + }); const selectableAlbumArtists = useMemo(() => { if (!albumArtistListQuery?.data?.items) return []; @@ -127,9 +141,12 @@ export const JellyfinAlbumFilters = ({ const albumArtistFilterString = e?.length ? e.join(',') : undefined; const updatedFilters = setFilter({ data: { - jfParams: { - ...filter.jfParams, - albumArtistIds: albumArtistFilterString, + _custom: { + ...filter._custom, + jellyfin: { + ...filter._custom?.jellyfin, + albumArtistIds: albumArtistFilterString, + }, }, }, key: pageKey, @@ -155,21 +172,21 @@ export const JellyfinAlbumFilters = ({ handleMinYearFilter(e)} /> handleMaxYearFilter(e)} /> @@ -189,7 +206,7 @@ export const JellyfinAlbumFilters = ({ clearable searchable data={selectableAlbumArtists} - defaultValue={filter.jfParams?.albumArtistIds?.split(',')} + defaultValue={filter._custom?.jellyfin?.albumArtistIds?.split(',')} disabled={disableArtistFilter} label="Artist" limit={300} diff --git a/src/renderer/features/albums/components/navidrome-album-filters.tsx b/src/renderer/features/albums/components/navidrome-album-filters.tsx index 70a69d00..a8d22eec 100644 --- a/src/renderer/features/albums/components/navidrome-album-filters.tsx +++ b/src/renderer/features/albums/components/navidrome-album-filters.tsx @@ -12,6 +12,7 @@ interface NavidromeAlbumFiltersProps { handleFilterChange: (filters: AlbumListFilter) => void; id?: string; pageKey: string; + serverId?: string; } export const NavidromeAlbumFilters = ({ @@ -19,15 +20,16 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter, pageKey, id, + serverId, }: NavidromeAlbumFiltersProps) => { const filter = useAlbumListFilter({ id, key: pageKey }); const { setFilter } = useListStoreActions(); - const genreListQuery = useGenreList(null); + const genreListQuery = useGenreList({ query: null, serverId }); const genreList = useMemo(() => { if (!genreListQuery?.data) return []; - return genreListQuery.data.map((genre) => ({ + return genreListQuery.data.items.map((genre) => ({ label: genre.name, value: genre.id, })); @@ -36,9 +38,12 @@ export const NavidromeAlbumFilters = ({ const handleGenresFilter = debounce((e: string | null) => { const updatedFilters = setFilter({ data: { - ndParams: { - ...filter.ndParams, - genre_id: e || undefined, + _custom: { + ...filter._custom, + navidrome: { + ...filter._custom?.navidrome, + genre_id: e || undefined, + }, }, }, key: 'album', @@ -52,70 +57,89 @@ export const NavidromeAlbumFilters = ({ onChange: (e: ChangeEvent) => { const updatedFilters = setFilter({ data: { - ndParams: { - ...filter.ndParams, - has_rating: e.currentTarget.checked ? true : undefined, + _custom: { + ...filter._custom, + navidrome: { + ...filter._custom?.navidrome, + has_rating: e.currentTarget.checked ? true : undefined, + }, }, }, key: pageKey, }) as AlbumListFilter; handleFilterChange(updatedFilters); }, - value: filter.ndParams?.has_rating, + value: filter._custom?.navidrome?.has_rating, }, { label: 'Is favorited', onChange: (e: ChangeEvent) => { + console.log('e.currentTarget.checked :>> ', e.currentTarget.checked); const updatedFilters = setFilter({ data: { - ndParams: { ...filter.ndParams, starred: e.currentTarget.checked ? true : undefined }, + _custom: { + ...filter._custom, + navidrome: { + ...filter._custom?.navidrome, + starred: e.currentTarget.checked ? true : undefined, + }, + }, }, key: pageKey, }) as AlbumListFilter; handleFilterChange(updatedFilters); }, - value: filter.ndParams?.starred, + value: filter._custom?.navidrome?.starred, }, { label: 'Is compilation', onChange: (e: ChangeEvent) => { const updatedFilters = setFilter({ data: { - ndParams: { - ...filter.ndParams, - compilation: e.currentTarget.checked ? true : undefined, + _custom: { + ...filter._custom, + navidrome: { + ...filter._custom?.navidrome, + compilation: e.currentTarget.checked ? true : undefined, + }, }, }, key: pageKey, }) as AlbumListFilter; handleFilterChange(updatedFilters); }, - value: filter.ndParams?.compilation, + value: filter._custom?.navidrome?.compilation, }, { label: 'Is recently played', onChange: (e: ChangeEvent) => { const updatedFilters = setFilter({ data: { - ndParams: { - ...filter.ndParams, - recently_played: e.currentTarget.checked ? true : undefined, + _custom: { + ...filter._custom, + navidrome: { + ...filter._custom?.navidrome, + recently_played: e.currentTarget.checked ? true : undefined, + }, }, }, key: pageKey, }) as AlbumListFilter; handleFilterChange(updatedFilters); }, - value: filter.ndParams?.recently_played, + value: filter._custom?.navidrome?.recently_played, }, ]; const handleYearFilter = debounce((e: number | string) => { const updatedFilters = setFilter({ data: { - ndParams: { - ...filter.ndParams, - year: e === '' ? undefined : (e as number), + _custom: { + navidrome: { + ...filter._custom?.navidrome, + year: e === '' ? undefined : (e as number), + }, + ...filter._custom, }, }, key: pageKey, @@ -125,18 +149,19 @@ export const NavidromeAlbumFilters = ({ const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState(''); - const albumArtistListQuery = useAlbumArtistList( - { + const albumArtistListQuery = useAlbumArtistList({ + options: { + cacheTime: 1000 * 60 * 2, + staleTime: 1000 * 60 * 1, + }, + query: { // searchTerm: debouncedSearchTerm, sortBy: AlbumArtistListSort.NAME, sortOrder: SortOrder.ASC, startIndex: 0, }, - { - cacheTime: 1000 * 60 * 2, - staleTime: 1000 * 60 * 1, - }, - ); + serverId, + }); const selectableAlbumArtists = useMemo(() => { if (!albumArtistListQuery?.data?.items) return []; @@ -150,9 +175,12 @@ export const NavidromeAlbumFilters = ({ const handleAlbumArtistFilter = (e: string | null) => { const updatedFilters = setFilter({ data: { - ndParams: { - ...filter.ndParams, - artist_id: e || undefined, + _custom: { + ...filter._custom, + navidrome: { + ...filter._custom?.navidrome, + artist_id: e || undefined, + }, }, }, key: pageKey, @@ -177,7 +205,7 @@ export const NavidromeAlbumFilters = ({ @@ -198,7 +226,7 @@ export const NavidromeAlbumFilters = ({ clearable searchable data={selectableAlbumArtists} - defaultValue={filter.ndParams?.artist_id} + defaultValue={filter._custom?.navidrome?.artist_id} disabled={disableArtistFilter} label="Artist" limit={300} diff --git a/src/renderer/features/albums/routes/album-detail-route.tsx b/src/renderer/features/albums/routes/album-detail-route.tsx index 5edb1b82..dc178b56 100644 --- a/src/renderer/features/albums/routes/album-detail-route.tsx +++ b/src/renderer/features/albums/routes/album-detail-route.tsx @@ -10,6 +10,7 @@ import { AlbumDetailHeader } from '/@/renderer/features/albums/components/album- import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { LibraryItem } from '/@/renderer/api/types'; +import { useCurrentServer } from '/@/renderer/store'; const AlbumDetailRoute = () => { const tableRef = useRef(null); @@ -17,7 +18,8 @@ const AlbumDetailRoute = () => { const headerRef = useRef(null); const { albumId } = useParams() as { albumId: string }; - const detailQuery = useAlbumDetail({ id: albumId }); + const server = useCurrentServer(); + const detailQuery = useAlbumDetail({ query: { id: albumId }, serverId: server?.id }); const background = useFastAverageColor(detailQuery.data?.imageUrl, !detailQuery.isLoading); const handlePlayQueueAdd = usePlayQueueAdd(); const playButtonBehavior = usePlayButtonBehavior(); diff --git a/src/renderer/features/albums/routes/album-list-route.tsx b/src/renderer/features/albums/routes/album-list-route.tsx index b2fcc1cc..ec2e83a7 100644 --- a/src/renderer/features/albums/routes/album-list-route.tsx +++ b/src/renderer/features/albums/routes/album-list-route.tsx @@ -1,4 +1,3 @@ -import { VirtualInfiniteGridRef } from '/@/renderer/components'; import { AnimatedPage } from '/@/renderer/features/shared'; import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header'; import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content'; @@ -8,6 +7,7 @@ import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-que import { generatePageKey, useAlbumListFilter, useCurrentServer } from '/@/renderer/store'; import { useParams, useSearchParams } from 'react-router-dom'; import { AlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; +import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; const AlbumListRoute = () => { const gridRef = useRef(null); @@ -24,17 +24,18 @@ const AlbumListRoute = () => { const albumListFilter = useAlbumListFilter({ id: albumArtistId || undefined, key: pageKey }); - const itemCountCheck = useAlbumList( - { + const itemCountCheck = useAlbumList({ + options: { + cacheTime: 1000 * 60, + staleTime: 1000 * 60, + }, + query: { limit: 1, startIndex: 0, ...albumListFilter, }, - { - cacheTime: 1000 * 60, - staleTime: 1000 * 60, - }, - ); + serverId: server?.id, + }); const itemCount = itemCountCheck.data?.totalRecordCount === null 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 89f22891..8526bf1a 100644 --- a/src/renderer/features/artists/components/album-artist-detail-content.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-content.tsx @@ -1,12 +1,5 @@ import { useMemo } from 'react'; -import { - Button, - getColumnDefs, - GridCarousel, - Text, - TextTitle, - VirtualTable, -} from '/@/renderer/components'; +import { Button, GridCarousel, Text, TextTitle } from '/@/renderer/components'; import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core'; import { Box, Group, Stack } from '@mantine/core'; import { RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri'; @@ -38,6 +31,7 @@ import { import { usePlayQueueAdd } from '/@/renderer/features/player'; import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query'; import { useTopSongsList } from '/@/renderer/features/artists/queries/top-songs-list-query'; +import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table'; const ContentContainer = styled.div` position: relative; @@ -63,7 +57,7 @@ export const AlbumArtistDetailContent = () => { const server = useCurrentServer(); const itemsPerPage = cq.isXl ? 9 : cq.isLg ? 7 : cq.isMd ? 5 : cq.isSm ? 4 : 3; - const detailQuery = useAlbumArtistDetail({ id: albumArtistId }); + const detailQuery = useAlbumArtistDetail({ query: { id: albumArtistId }, serverId: server?.id }); const artistDiscographyLink = `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY, { albumArtistId, @@ -80,34 +74,57 @@ export const AlbumArtistDetailContent = () => { })}`; const recentAlbumsQuery = useAlbumList({ - jfParams: server?.type === ServerType.JELLYFIN ? { artistIds: albumArtistId } : undefined, - limit: itemsPerPage, - ndParams: - server?.type === ServerType.NAVIDROME - ? { artist_id: albumArtistId, compilation: false } - : undefined, - sortBy: AlbumListSort.RELEASE_DATE, - sortOrder: SortOrder.DESC, - startIndex: 0, + query: { + _custom: { + jellyfin: { + ...(server?.type === ServerType.JELLYFIN ? { artistIds: albumArtistId } : undefined), + }, + navidrome: { + ...(server?.type === ServerType.NAVIDROME + ? { artist_id: albumArtistId, compilation: false } + : undefined), + }, + }, + limit: itemsPerPage, + sortBy: AlbumListSort.RELEASE_DATE, + sortOrder: SortOrder.DESC, + startIndex: 0, + }, + serverId: server?.id, }); const compilationAlbumsQuery = useAlbumList({ - jfParams: - server?.type === ServerType.JELLYFIN ? { contributingArtistIds: albumArtistId } : undefined, - limit: itemsPerPage, - ndParams: - server?.type === ServerType.NAVIDROME - ? { artist_id: albumArtistId, compilation: true } - : undefined, - sortBy: AlbumListSort.RELEASE_DATE, - sortOrder: SortOrder.DESC, - startIndex: 0, + query: { + _custom: { + jellyfin: { + ...(server?.type === ServerType.JELLYFIN + ? { contributingArtistIds: albumArtistId } + : undefined), + }, + navidrome: { + ...(server?.type === ServerType.NAVIDROME + ? { artist_id: albumArtistId, compilation: true } + : undefined), + }, + }, + limit: itemsPerPage, + sortBy: AlbumListSort.RELEASE_DATE, + sortOrder: SortOrder.DESC, + startIndex: 0, + }, + serverId: server?.id, }); - const topSongsQuery = useTopSongsList( - { artist: detailQuery?.data?.name || '', artistId: albumArtistId }, - { enabled: !!detailQuery?.data?.name }, - ); + const topSongsQuery = useTopSongsList({ + options: { + enabled: !!detailQuery?.data?.name, + }, + query: { + artist: detailQuery?.data?.name || '', + artistId: albumArtistId, + }, + serverId: server?.id, + }); const topSongsColumnDefs: ColDef[] = useMemo( () => @@ -242,8 +259,8 @@ export const AlbumArtistDetailContent = () => { }); }; - const createFavoriteMutation = useCreateFavorite(); - const deleteFavoriteMutation = useDeleteFavorite(); + const createFavoriteMutation = useCreateFavorite({}); + const deleteFavoriteMutation = useDeleteFavorite({}); const handleFavorite = () => { if (!detailQuery?.data) return; diff --git a/src/renderer/features/artists/components/album-artist-detail-header.tsx b/src/renderer/features/artists/components/album-artist-detail-header.tsx index 62c37258..92dddc17 100644 --- a/src/renderer/features/artists/components/album-artist-detail-header.tsx +++ b/src/renderer/features/artists/components/album-artist-detail-header.tsx @@ -1,13 +1,14 @@ -import { Group, Rating, Stack } from '@mantine/core'; import { forwardRef, Fragment, Ref, MouseEvent } from 'react'; +import { Group, Rating, Stack } from '@mantine/core'; import { useParams } from 'react-router'; import { LibraryItem, ServerType } from '/@/renderer/api/types'; import { Text } from '/@/renderer/components'; import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query'; -import { LibraryHeader, useUpdateRating } from '/@/renderer/features/shared'; +import { LibraryHeader, useSetRating } from '/@/renderer/features/shared'; import { useContainerQuery } from '/@/renderer/hooks'; import { AppRoute } from '/@/renderer/router/routes'; import { formatDurationString } from '/@/renderer/utils'; +import { useCurrentServer } from '../../../store/auth.store'; interface AlbumArtistDetailHeaderProps { background: string; @@ -16,7 +17,11 @@ interface AlbumArtistDetailHeaderProps { export const AlbumArtistDetailHeader = forwardRef( ({ background }: AlbumArtistDetailHeaderProps, ref: Ref) => { const { albumArtistId } = useParams() as { albumArtistId: string }; - const detailQuery = useAlbumArtistDetail({ id: albumArtistId }); + const server = useCurrentServer(); + const detailQuery = useAlbumArtistDetail({ + query: { id: albumArtistId }, + serverId: server?.id, + }); const cq = useContainerQuery(); const metadataItems = [ @@ -37,17 +42,17 @@ export const AlbumArtistDetailHeader = forwardRef( }, ]; - const updateRatingMutation = useUpdateRating(); + const updateRatingMutation = useSetRating({}); const handleUpdateRating = (rating: number) => { if (!detailQuery?.data) return; updateRatingMutation.mutate({ - _serverId: detailQuery?.data.serverId, query: { item: [detailQuery.data], rating, }, + serverId: detailQuery?.data.serverId, }); }; @@ -58,11 +63,11 @@ export const AlbumArtistDetailHeader = forwardRef( if (!isSameRatingAsPrevious) return; updateRatingMutation.mutate({ - _serverId: detailQuery.data.serverId, query: { item: [detailQuery.data], rating: 0, }, + serverId: detailQuery.data.serverId, }); }; 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 b151fc90..987504b0 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 @@ -1,13 +1,14 @@ import { MutableRefObject, useMemo } from 'react'; import type { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; -import { getColumnDefs, VirtualGridAutoSizerContainer, VirtualTable } from '/@/renderer/components'; import { useCurrentServer, useSongListStore } from '/@/renderer/store'; import { useHandleTableContextMenu } from '/@/renderer/features/context-menu'; import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { LibraryItem, QueueSong } from '/@/renderer/api/types'; import { usePlayQueueAdd } from '/@/renderer/features/player'; +import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid'; +import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table'; interface AlbumArtistSongListContentProps { data: QueueSong[]; diff --git a/src/renderer/features/artists/components/album-artist-list-content.tsx b/src/renderer/features/artists/components/album-artist-list-content.tsx index 2bfb4688..2ffdc7c1 100644 --- a/src/renderer/features/artists/components/album-artist-list-content.tsx +++ b/src/renderer/features/artists/components/album-artist-list-content.tsx @@ -1,12 +1,4 @@ -import { - ALBUMARTIST_CARD_ROWS, - getColumnDefs, - TablePagination, - VirtualGridAutoSizerContainer, - VirtualInfiniteGrid, - VirtualInfiniteGridRef, - VirtualTable, -} from '/@/renderer/components'; +import { ALBUMARTIST_CARD_ROWS } from '/@/renderer/components'; import { AppRoute } from '/@/renderer/router/routes'; import { ListDisplayType, CardRow } from '/@/renderer/types'; import AutoSizer from 'react-virtualized-auto-sizer'; @@ -35,6 +27,12 @@ import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-a import { usePlayQueueAdd } from '/@/renderer/features/player'; import { useAlbumArtistListFilter, useListStoreActions } from '../../../store/list.store'; import { useAlbumArtistListContext } from '/@/renderer/features/artists/context/album-artist-list-context'; +import { + VirtualInfiniteGridRef, + VirtualGridAutoSizerContainer, + VirtualInfiniteGrid, +} from '/@/renderer/components/virtual-grid'; +import { getColumnDefs, VirtualTable, TablePagination } from '/@/renderer/components/virtual-table'; interface AlbumArtistListContentProps { gridRef: MutableRefObject; @@ -54,17 +52,18 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon const isPaginationEnabled = display === ListDisplayType.TABLE_PAGINATED; - const checkAlbumArtistList = useAlbumArtistList( - { + const checkAlbumArtistList = useAlbumArtistList({ + options: { + cacheTime: Infinity, + staleTime: 60 * 1000 * 5, + }, + query: { limit: 1, startIndex: 0, ...filter, }, - { - cacheTime: Infinity, - staleTime: 60 * 1000 * 5, - }, - ); + serverId: server?.id, + }); const columnDefs: ColDef[] = useMemo(() => getColumnDefs(table.columns), [table.columns]); @@ -85,19 +84,23 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon queryKey, async ({ signal }) => api.controller.getAlbumArtistList({ + apiClientProps: { + server, + signal, + }, query: { limit, startIndex, ...filter, }, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); - const albums = api.normalize.albumArtistList(albumArtistsRes, server); - params.successCallback(albums?.items || [], albumArtistsRes?.totalRecordCount || 0); + params.successCallback( + albumArtistsRes?.items || [], + albumArtistsRes?.totalRecordCount || 0, + ); }, rowCount: undefined, }; @@ -181,18 +184,20 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon queryKey, async ({ signal }) => api.controller.getAlbumArtistList({ + apiClientProps: { + server, + signal, + }, query: { limit, startIndex, ...filter, }, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); - return api.normalize.albumArtistList(albumArtistsRes, server); + return albumArtistsRes; }, [filter, queryClient, server], ); @@ -259,27 +264,29 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon {display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? ( {({ height, width }) => ( - + <> + + )} ) : ( diff --git a/src/renderer/features/artists/components/album-artist-list-header-filters.tsx b/src/renderer/features/artists/components/album-artist-list-header-filters.tsx index eb3580a4..a9865bdc 100644 --- a/src/renderer/features/artists/components/album-artist-list-header-filters.tsx +++ b/src/renderer/features/artists/components/album-artist-list-header-filters.tsx @@ -15,16 +15,7 @@ import { import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; import { AlbumArtistListSort, SortOrder } from '/@/renderer/api/types'; -import { - DropdownMenu, - ALBUMARTIST_TABLE_COLUMNS, - VirtualInfiniteGridRef, - Text, - Button, - Slider, - MultiSelect, - Switch, -} from '/@/renderer/components'; +import { DropdownMenu, Text, Button, Slider, MultiSelect, Switch } from '/@/renderer/components'; import { useMusicFolders } from '/@/renderer/features/shared'; import { useContainerQuery } from '/@/renderer/hooks'; import { @@ -36,6 +27,8 @@ import { } from '/@/renderer/store'; import { ListDisplayType, TableColumn, ServerType } from '/@/renderer/types'; import { useAlbumArtistListContext } from '../context/album-artist-list-context'; +import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; +import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; const FILTERS = { jellyfin: [ @@ -83,7 +76,7 @@ export const AlbumArtistListHeaderFilters = ({ const filter = useAlbumArtistListFilter({ key: pageKey }); const cq = useContainerQuery(); - const musicFoldersQuery = useMusicFolders(); + const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id }); const sortByLabel = (server?.type && @@ -114,18 +107,20 @@ export const AlbumArtistListHeaderFilters = ({ queryKey, async ({ signal }) => api.controller.getAlbumArtistList({ + apiClientProps: { + server, + signal, + }, query: { limit, startIndex, ...filters, }, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); - return api.normalize.albumArtistList(albums, server); + return albums; }, [queryClient, server], ); @@ -148,20 +143,21 @@ export const AlbumArtistListHeaderFilters = ({ queryKey, async ({ signal }) => api.controller.getAlbumArtistList({ + apiClientProps: { + server, + signal, + }, query: { limit, startIndex, ...filters, }, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); - const albumArtists = api.normalize.albumArtistList(albumArtistsRes, server); params.successCallback( - albumArtists?.items || [], + albumArtistsRes?.items || [], albumArtistsRes?.totalRecordCount || 0, ); }, @@ -355,7 +351,7 @@ export const AlbumArtistListHeaderFilters = ({ - {musicFoldersQuery.data?.map((folder) => ( + {musicFoldersQuery.data?.items.map((folder) => ( ; @@ -51,18 +52,20 @@ export const AlbumArtistListHeader = ({ queryKey, async ({ signal }) => api.controller.getAlbumArtistList({ + apiClientProps: { + server, + signal, + }, query: { limit, startIndex, ...filters, }, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); - return api.normalize.albumArtistList(albums, server); + return albums; }, [queryClient, server], ); @@ -85,20 +88,21 @@ export const AlbumArtistListHeader = ({ queryKey, async ({ signal }) => api.controller.getAlbumArtistList({ + apiClientProps: { + server, + signal, + }, query: { limit, startIndex, ...filters, }, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); - const albumArtists = api.normalize.albumArtistList(albumArtistsRes, server); params.successCallback( - albumArtists?.items || [], + albumArtistsRes?.items || [], albumArtistsRes?.totalRecordCount || 0, ); }, diff --git a/src/renderer/features/artists/routes/album-artist-detail-route.tsx b/src/renderer/features/artists/routes/album-artist-detail-route.tsx index faa53f3f..dcbff694 100644 --- a/src/renderer/features/artists/routes/album-artist-detail-route.tsx +++ b/src/renderer/features/artists/routes/album-artist-detail-route.tsx @@ -9,15 +9,17 @@ import { LibraryItem } from '/@/renderer/api/types'; import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query'; import { AlbumArtistDetailHeader } from '/@/renderer/features/artists/components/album-artist-detail-header'; import { AlbumArtistDetailContent } from '/@/renderer/features/artists/components/album-artist-detail-content'; +import { useCurrentServer } from '/@/renderer/store'; const AlbumArtistDetailRoute = () => { const scrollAreaRef = useRef(null); const headerRef = useRef(null); + const server = useCurrentServer(); const { albumArtistId } = useParams() as { albumArtistId: string }; const handlePlayQueueAdd = usePlayQueueAdd(); const playButtonBehavior = usePlayButtonBehavior(); - const detailQuery = useAlbumArtistDetail({ id: albumArtistId }); + const detailQuery = useAlbumArtistDetail({ query: { id: albumArtistId }, serverId: server?.id }); const background = useFastAverageColor(detailQuery.data?.imageUrl, !detailQuery.isLoading); const handlePlay = () => { diff --git a/src/renderer/features/artists/routes/album-artist-detail-top-songs-list-route.tsx b/src/renderer/features/artists/routes/album-artist-detail-top-songs-list-route.tsx index bf22e974..e6835c1a 100644 --- a/src/renderer/features/artists/routes/album-artist-detail-top-songs-list-route.tsx +++ b/src/renderer/features/artists/routes/album-artist-detail-top-songs-list-route.tsx @@ -1,22 +1,25 @@ -import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { useRef } from 'react'; +import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { useParams } from 'react-router'; import { AlbumArtistDetailTopSongsListContent } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-content'; import { AlbumArtistDetailTopSongsListHeader } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-header'; import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query'; import { useTopSongsList } from '/@/renderer/features/artists/queries/top-songs-list-query'; import { AnimatedPage } from '/@/renderer/features/shared'; +import { useCurrentServer } from '../../../store/auth.store'; const AlbumArtistDetailTopSongsListRoute = () => { const tableRef = useRef(null); const { albumArtistId } = useParams() as { albumArtistId: string }; + const server = useCurrentServer(); - const detailQuery = useAlbumArtistDetail({ id: albumArtistId }); + const detailQuery = useAlbumArtistDetail({ query: { id: albumArtistId }, serverId: server?.id }); - const topSongsQuery = useTopSongsList( - { artist: detailQuery?.data?.name || '', artistId: albumArtistId }, - { enabled: !!detailQuery?.data?.name }, - ); + const topSongsQuery = useTopSongsList({ + options: { enabled: !!detailQuery?.data?.name }, + query: { artist: detailQuery?.data?.name || '', artistId: albumArtistId }, + serverId: server?.id, + }); const itemCount = topSongsQuery?.data?.items?.length || 0; diff --git a/src/renderer/features/artists/routes/album-artist-list-route.tsx b/src/renderer/features/artists/routes/album-artist-list-route.tsx index 7d56b955..37bc6a59 100644 --- a/src/renderer/features/artists/routes/album-artist-list-route.tsx +++ b/src/renderer/features/artists/routes/album-artist-list-route.tsx @@ -1,4 +1,3 @@ -import { VirtualInfiniteGridRef } from '/@/renderer/components'; import { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header'; import { AnimatedPage } from '/@/renderer/features/shared'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; @@ -7,25 +6,29 @@ import { AlbumArtistListContent } from '/@/renderer/features/artists/components/ import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query'; import { generatePageKey, useAlbumArtistListFilter } from '/@/renderer/store'; import { AlbumArtistListContext } from '/@/renderer/features/artists/context/album-artist-list-context'; +import { useCurrentServer } from '../../../store/auth.store'; +import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; const AlbumArtistListRoute = () => { const gridRef = useRef(null); const tableRef = useRef(null); const pageKey = generatePageKey('albumArtist', undefined); + const server = useCurrentServer(); const albumArtistListFilter = useAlbumArtistListFilter({ id: undefined, key: pageKey }); - const itemCountCheck = useAlbumArtistList( - { + const itemCountCheck = useAlbumArtistList({ + options: { + cacheTime: 1000 * 60, + staleTime: 1000 * 60, + }, + query: { limit: 1, startIndex: 0, ...albumArtistListFilter, }, - { - cacheTime: 1000 * 60, - staleTime: 1000 * 60, - }, - ); + serverId: server?.id, + }); const itemCount = itemCountCheck.data?.totalRecordCount === null diff --git a/src/renderer/features/context-menu/context-menu-provider.tsx b/src/renderer/features/context-menu/context-menu-provider.tsx index 255fe0b1..75d07d36 100644 --- a/src/renderer/features/context-menu/context-menu-provider.tsx +++ b/src/renderer/features/context-menu/context-menu-provider.tsx @@ -43,7 +43,7 @@ import { import { usePlayQueueAdd } from '/@/renderer/features/player'; import { useDeletePlaylist } from '/@/renderer/features/playlists'; import { useRemoveFromPlaylist } from '/@/renderer/features/playlists/mutations/remove-from-playlist-mutation'; -import { useCreateFavorite, useDeleteFavorite, useUpdateRating } from '/@/renderer/features/shared'; +import { useCreateFavorite, useDeleteFavorite, useSetRating } from '/@/renderer/features/shared'; import { useAuthStore, useCurrentServer, useQueueControls } from '/@/renderer/store'; import { usePlayerType } from '/@/renderer/store/settings.store'; import { Play, PlaybackType } from '/@/renderer/types'; @@ -190,7 +190,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { [ctx.data, ctx.type, handlePlayQueueAdd], ); - const deletePlaylistMutation = useDeletePlaylist(); + const deletePlaylistMutation = useDeletePlaylist({}); const handleDeletePlaylist = useCallback(() => { for (const item of ctx.data) { @@ -236,8 +236,8 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { }); }, [ctx.data, handleDeletePlaylist]); - const createFavoriteMutation = useCreateFavorite(); - const deleteFavoriteMutation = useDeleteFavorite(); + const createFavoriteMutation = useCreateFavorite({}); + const deleteFavoriteMutation = useDeleteFavorite({}); const handleAddToFavorites = useCallback(() => { if (!ctx.dataNodes && !ctx.data) return; @@ -414,7 +414,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { serverType, ]); - const updateRatingMutation = useUpdateRating(); + const updateRatingMutation = useSetRating({}); const handleUpdateRating = useCallback( (rating: number) => { @@ -450,11 +450,11 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => { updateRatingMutation.mutate( { - _serverId: serverId, query: { item: items, rating, }, + serverId, }, { onSuccess: () => { diff --git a/src/renderer/features/home/routes/home-route.tsx b/src/renderer/features/home/routes/home-route.tsx index 1b0ef061..e7805c20 100644 --- a/src/renderer/features/home/routes/home-route.tsx +++ b/src/renderer/features/home/routes/home-route.tsx @@ -23,75 +23,80 @@ const HomeRoute = () => { recentlyPlayed: 0, }); - const feature = useAlbumList( - { + const feature = useAlbumList({ + options: { + cacheTime: 1000 * 60, + staleTime: 1000 * 60, + }, + query: { limit: 20, sortBy: AlbumListSort.RANDOM, sortOrder: SortOrder.DESC, startIndex: 0, }, - { - cacheTime: 1000 * 60, - staleTime: 1000 * 60, - }, - ); + serverId: server?.id, + }); const featureItemsWithImage = useMemo(() => { return feature.data?.items?.filter((item) => item.imageUrl) ?? []; }, [feature.data?.items]); - const random = useAlbumList( - { + const random = useAlbumList({ + options: { + cacheTime: 1000 * 60, + keepPreviousData: true, + staleTime: 1000 * 60, + }, + query: { limit: itemsPerPage, sortBy: AlbumListSort.RANDOM, sortOrder: SortOrder.ASC, startIndex: pagination.random * itemsPerPage, }, - { - cacheTime: 1000 * 60, - keepPreviousData: true, - staleTime: 1000 * 60, - }, - ); + serverId: server?.id, + }); - const recentlyPlayed = useRecentlyPlayed( - { + const recentlyPlayed = useRecentlyPlayed({ + options: { + keepPreviousData: true, + staleTime: 0, + }, + query: { limit: itemsPerPage, sortBy: AlbumListSort.RECENTLY_PLAYED, sortOrder: SortOrder.DESC, startIndex: pagination.recentlyPlayed * itemsPerPage, }, - { - keepPreviousData: true, - staleTime: 0, - }, - ); + serverId: server?.id, + }); - const recentlyAdded = useAlbumList( - { + const recentlyAdded = useAlbumList({ + options: { + keepPreviousData: true, + staleTime: 1000 * 60, + }, + query: { limit: itemsPerPage, sortBy: AlbumListSort.RECENTLY_ADDED, sortOrder: SortOrder.DESC, startIndex: pagination.recentlyAdded * itemsPerPage, }, - { - keepPreviousData: true, - staleTime: 1000 * 60, - }, - ); + serverId: server?.id, + }); - const mostPlayed = useAlbumList( - { + const mostPlayed = useAlbumList({ + options: { + keepPreviousData: true, + staleTime: 1000 * 60 * 60, + }, + query: { limit: itemsPerPage, sortBy: AlbumListSort.PLAY_COUNT, sortOrder: SortOrder.DESC, startIndex: pagination.mostPlayed * itemsPerPage, }, - { - keepPreviousData: true, - staleTime: 1000 * 60 * 60, - }, - ); + serverId: server?.id, + }); const handleNextPage = useCallback( (key: 'mostPlayed' | 'random' | 'recentlyAdded' | 'recentlyPlayed') => { diff --git a/src/renderer/features/now-playing/components/play-queue-list-controls.tsx b/src/renderer/features/now-playing/components/play-queue-list-controls.tsx index 0d40c705..b97456c9 100644 --- a/src/renderer/features/now-playing/components/play-queue-list-controls.tsx +++ b/src/renderer/features/now-playing/components/play-queue-list-controls.tsx @@ -1,7 +1,7 @@ import type { MutableRefObject } from 'react'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { Group } from '@mantine/core'; -import { Button, Popover, TableConfigDropdown } from '/@/renderer/components'; +import { Button, Popover } from '/@/renderer/components'; import isElectron from 'is-electron'; import { RiArrowDownLine, @@ -16,6 +16,7 @@ import { usePlayerControls, useQueueControls } from '/@/renderer/store'; import { PlaybackType, TableType } from '/@/renderer/types'; import { usePlayerType } from '/@/renderer/store/settings.store'; import { useSetCurrentTime } from '../../../store/player.store'; +import { TableConfigDropdown } from '/@/renderer/components/virtual-table'; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; diff --git a/src/renderer/features/now-playing/components/play-queue.tsx b/src/renderer/features/now-playing/components/play-queue.tsx index 0a29c2c3..fde31ae2 100644 --- a/src/renderer/features/now-playing/components/play-queue.tsx +++ b/src/renderer/features/now-playing/components/play-queue.tsx @@ -8,7 +8,6 @@ import type { } from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import '@ag-grid-community/styles/ag-theme-alpine.css'; -import { VirtualGridAutoSizerContainer, getColumnDefs } from '/@/renderer/components'; import { useAppStoreActions, useCurrentSong, @@ -27,12 +26,13 @@ import { useMergedRef } from '@mantine/hooks'; import isElectron from 'is-electron'; import debounce from 'lodash/debounce'; import { ErrorBoundary } from 'react-error-boundary'; -import { VirtualTable } from '/@/renderer/components/virtual-table'; +import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table'; import { ErrorFallback } from '/@/renderer/features/action-required'; import { PlaybackType, TableType } from '/@/renderer/types'; import { LibraryItem, QueueSong } from '/@/renderer/api/types'; import { useHandleTableContextMenu } from '/@/renderer/features/context-menu'; import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; +import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid'; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; const utils = isElectron() ? window.electron.utils : null; diff --git a/src/renderer/features/now-playing/routes/now-playing-route.tsx b/src/renderer/features/now-playing/routes/now-playing-route.tsx index d9807126..b393acbc 100644 --- a/src/renderer/features/now-playing/routes/now-playing-route.tsx +++ b/src/renderer/features/now-playing/routes/now-playing-route.tsx @@ -4,8 +4,9 @@ import { NowPlayingHeader } from '/@/renderer/features/now-playing/components/no import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue'; import type { Song } from '/@/renderer/api/types'; import { AnimatedPage } from '/@/renderer/features/shared'; -import { Paper, VirtualGridContainer } from '/@/renderer/components'; +import { Paper } from '/@/renderer/components'; import { PlayQueueListControls } from '/@/renderer/features/now-playing/components/play-queue-list-controls'; +import { VirtualGridContainer } from '/@/renderer/components/virtual-grid'; const NowPlayingRoute = () => { const queueRef = useRef<{ grid: AgGridReactType } | null>(null); diff --git a/src/renderer/features/player/components/full-screen-player.tsx b/src/renderer/features/player/components/full-screen-player.tsx index 057d4666..c64cfb3b 100644 --- a/src/renderer/features/player/components/full-screen-player.tsx +++ b/src/renderer/features/player/components/full-screen-player.tsx @@ -5,7 +5,7 @@ import { Variants, motion } from 'framer-motion'; import { RiArrowDownSLine, RiSettings3Line } from 'react-icons/ri'; import { useLocation } from 'react-router'; import styled from 'styled-components'; -import { Button, Option, Popover, Switch, TableConfigDropdown } from '/@/renderer/components'; +import { Button, Option, Popover, Switch } from '/@/renderer/components'; import { useCurrentSong, useFullScreenPlayerStore, @@ -14,6 +14,7 @@ import { import { useFastAverageColor } from '../../../hooks/use-fast-average-color'; import { FullScreenPlayerImage } from '/@/renderer/features/player/components/full-screen-player-image'; import { FullScreenPlayerQueue } from '/@/renderer/features/player/components/full-screen-player-queue'; +import { TableConfigDropdown } from '/@/renderer/components/virtual-table'; const Container = styled(motion.div)` z-index: 100; diff --git a/src/renderer/features/player/components/right-controls.tsx b/src/renderer/features/player/components/right-controls.tsx index 2a167030..75e47476 100644 --- a/src/renderer/features/player/components/right-controls.tsx +++ b/src/renderer/features/player/components/right-controls.tsx @@ -19,7 +19,7 @@ import { import { useRightControls } from '../hooks/use-right-controls'; import { PlayerButton } from './player-button'; import { LibraryItem, ServerType } from '/@/renderer/api/types'; -import { useCreateFavorite, useDeleteFavorite, useUpdateRating } from '/@/renderer/features/shared'; +import { useCreateFavorite, useDeleteFavorite, useSetRating } from '/@/renderer/features/shared'; import { Rating } from '/@/renderer/components'; import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider'; @@ -32,9 +32,9 @@ export const RightControls = () => { const { rightExpanded: isQueueExpanded } = useSidebarStore(); const { handleVolumeSlider, handleVolumeWheel, handleMute } = useRightControls(); - const updateRatingMutation = useUpdateRating(); - const addToFavoritesMutation = useCreateFavorite(); - const removeFromFavoritesMutation = useDeleteFavorite(); + const updateRatingMutation = useSetRating({}); + const addToFavoritesMutation = useCreateFavorite({}); + const removeFromFavoritesMutation = useDeleteFavorite({}); const handleAddToFavorites = () => { if (!currentSong) return; @@ -51,11 +51,11 @@ export const RightControls = () => { if (!currentSong) return; updateRatingMutation.mutate({ - _serverId: currentSong?.serverId, query: { item: [currentSong], rating, }, + serverId: currentSong?.serverId, }); }; @@ -63,11 +63,11 @@ export const RightControls = () => { if (!currentSong || !rating) return; updateRatingMutation.mutate({ - _serverId: currentSong?.serverId, query: { item: [currentSong], rating: 0, }, + serverId: currentSong?.serverId, }); }; diff --git a/src/renderer/features/player/hooks/use-handle-playqueue-add.ts b/src/renderer/features/player/hooks/use-handle-playqueue-add.ts index 60157e51..1d0575da 100644 --- a/src/renderer/features/player/hooks/use-handle-playqueue-add.ts +++ b/src/renderer/features/player/hooks/use-handle-playqueue-add.ts @@ -1,20 +1,11 @@ import { useCallback } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { api } from '/@/renderer/api/index'; -import { jfNormalize } from '/@/renderer/api/jellyfin.api'; -import { JFSong } from '/@/renderer/api/jellyfin.types'; -import { ndNormalize } from '/@/renderer/api/navidrome.api'; -import { NDSong } from '/@/renderer/api/navidrome.types'; import { queryKeys } from '/@/renderer/api/query-keys'; -import { - useAuthStore, - useCurrentServer, - usePlayerControls, - usePlayerStore, -} from '/@/renderer/store'; +import { useCurrentServer, usePlayerControls, usePlayerStore } from '/@/renderer/store'; import { usePlayerType } from '/@/renderer/store/settings.store'; import { PlayQueueAddOptions, Play, PlaybackType } from '/@/renderer/types'; -import { toast } from '/@/renderer/components/toast'; +import { toast } from '/@/renderer/components/toast/index'; import isElectron from 'is-electron'; import { nanoid } from 'nanoid/non-secure'; import { LibraryItem, SongListSort, SortOrder } from '/@/renderer/api/types'; @@ -25,7 +16,6 @@ const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null; export const useHandlePlayQueueAdd = () => { const queryClient = useQueryClient(); const playerType = usePlayerType(); - const deviceId = useAuthStore.getState().deviceId; const server = useCurrentServer(); const { play } = usePlayerControls(); @@ -114,9 +104,11 @@ export const useHandlePlayQueueAdd = () => { queryKey, async ({ signal }) => api.controller.getPlaylistSongList({ + apiClientProps: { + server, + signal, + }, query: queryFilter, - server, - signal, }), { cacheTime: 1000 * 60, @@ -128,9 +120,11 @@ export const useHandlePlayQueueAdd = () => { queryKey, async ({ signal }) => api.controller.getSongList({ + apiClientProps: { + server, + signal, + }, query: queryFilter, - server, - signal, }), { cacheTime: 1000 * 60, @@ -147,20 +141,7 @@ export const useHandlePlayQueueAdd = () => { if (!songsList) return toast.warn({ message: 'No songs found' }); - switch (server?.type) { - case 'jellyfin': - songs = songsList.items?.map((song) => - jfNormalize.song(song as JFSong, server, deviceId), - ); - break; - case 'navidrome': - songs = songsList.items?.map((song) => - ndNormalize.song(song as NDSong, server, deviceId), - ); - break; - case 'subsonic': - break; - } + songs = songsList.items?.map((song) => ({ ...song, uniqueId: nanoid() })); } else if (options.byData) { songs = options.byData.map((song) => ({ ...song, uniqueId: nanoid() })); } @@ -207,7 +188,7 @@ export const useHandlePlayQueueAdd = () => { return null; }, - [deviceId, play, playerType, queryClient, server], + [play, playerType, queryClient, server], ); return handlePlayQueueAdd; diff --git a/src/renderer/features/player/hooks/use-scrobble.ts b/src/renderer/features/player/hooks/use-scrobble.ts index 7c8d7353..5d11475f 100644 --- a/src/renderer/features/player/hooks/use-scrobble.ts +++ b/src/renderer/features/player/hooks/use-scrobble.ts @@ -67,13 +67,13 @@ export const useScrobble = () => { currentSong?.serverType === ServerType.JELLYFIN ? currentTime * 1e7 : undefined; sendScrobble.mutate({ - _serverId: currentSong?.serverId, query: { event: 'timeupdate', id: currentSong.id, position, submission: false, }, + serverId: currentSong?.serverId, }); }, [isScrobbleEnabled, sendScrobble], @@ -110,12 +110,12 @@ export const useScrobble = () => { previousSong?.serverType === ServerType.JELLYFIN ? previousSongTime * 1e7 : undefined; sendScrobble.mutate({ - _serverId: previousSong?.serverId, query: { id: previousSong.id, position, submission: true, }, + serverId: previousSong?.serverId, }); } } @@ -130,13 +130,13 @@ export const useScrobble = () => { // Send start scrobble when song changes and the new song is playing if (status === PlayerStatus.PLAYING && currentSong?.id) { sendScrobble.mutate({ - _serverId: currentSong?.serverId, query: { event: 'start', id: currentSong.id, position: 0, submission: false, }, + serverId: currentSong?.serverId, }); if (currentSong?.serverType === ServerType.JELLYFIN) { @@ -175,13 +175,13 @@ export const useScrobble = () => { // Whenever the player is restarted, send a 'start' scrobble if (status === PlayerStatus.PLAYING) { sendScrobble.mutate({ - _serverId: currentSong?.serverId, query: { event: 'unpause', id: currentSong.id, position, submission: false, }, + serverId: currentSong?.serverId, }); if (currentSong?.serverType === ServerType.JELLYFIN) { @@ -194,13 +194,13 @@ export const useScrobble = () => { // Jellyfin is the only one that needs to send a 'pause' event to the server } else if (currentSong?.serverType === ServerType.JELLYFIN) { sendScrobble.mutate({ - _serverId: currentSong?.serverId, query: { event: 'pause', id: currentSong.id, position, submission: false, }, + serverId: currentSong?.serverId, }); if (progressIntervalId.current) { @@ -217,11 +217,11 @@ export const useScrobble = () => { if (!isCurrentSongScrobbled && shouldSubmitScrobble) { sendScrobble.mutate({ - _serverId: currentSong?.serverId, query: { id: currentSong.id, submission: true, }, + serverId: currentSong?.serverId, }); setIsCurrentSongScrobbled(true); @@ -261,24 +261,24 @@ export const useScrobble = () => { if (!isCurrentSongScrobbled && shouldSubmitScrobble) { sendScrobble.mutate({ - _serverId: currentSong?.serverId, query: { id: currentSong.id, position, submission: true, }, + serverId: currentSong?.serverId, }); } if (currentSong?.serverType === ServerType.JELLYFIN) { sendScrobble.mutate({ - _serverId: currentSong?.serverId, query: { event: 'start', id: currentSong.id, position: 0, submission: false, }, + serverId: currentSong?.serverId, }); } 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 1d46b4eb..69774827 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 @@ -23,15 +23,20 @@ export const AddToPlaylistContextModal = ({ const server = useCurrentServer(); const [isLoading, setIsLoading] = useState(false); - const addToPlaylistMutation = useAddToPlaylist(); + const addToPlaylistMutation = useAddToPlaylist({}); const playlistList = usePlaylistList({ - ndParams: { - smart: false, + query: { + _custom: { + navidrome: { + smart: false, + }, + }, + sortBy: PlaylistListSort.NAME, + sortOrder: SortOrder.ASC, + startIndex: 0, }, - sortBy: PlaylistListSort.NAME, - sortOrder: SortOrder.ASC, - startIndex: 0, + serverId: server?.id, }); const playlistSelect = useMemo(() => { @@ -60,11 +65,12 @@ export const AddToPlaylistContextModal = ({ const queryKey = queryKeys.songs.list(server?.id || '', query); - const songsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => - api.controller.getSongList({ query, server, signal }), - ); + const songsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => { + if (!server) throw new Error('No server'); + return api.controller.getSongList({ apiClientProps: { server, signal }, query }); + }); - return api.normalize.songList(songsRes, server); + return songsRes; }; const getSongsByArtist = async (artistId: string) => { @@ -77,11 +83,12 @@ export const AddToPlaylistContextModal = ({ const queryKey = queryKeys.songs.list(server?.id || '', query); - const songsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => - api.controller.getSongList({ query, server, signal }), - ); + const songsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => { + if (!server) throw new Error('No server'); + return api.controller.getSongList({ apiClientProps: { server, signal }, query }); + }); - return api.normalize.songList(songsRes, server); + return songsRes; }; const isSubmitDisabled = form.values.playlistId.length === 0 || addToPlaylistMutation.isLoading; @@ -118,17 +125,18 @@ export const AddToPlaylistContextModal = ({ const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId, query); - const playlistSongsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => - api.controller.getPlaylistSongList({ + const playlistSongsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => { + if (!server) throw new Error('No server'); + return api.controller.getPlaylistSongList({ + apiClientProps: { + server, + signal, + }, query: { id: playlistId, startIndex: 0 }, - server, - signal, - }), - ); + }); + }); - const playlistSongIds = api.normalize - .songList(playlistSongsRes, server) - .items?.map((song) => song.id); + const playlistSongIds = playlistSongsRes?.items?.map((song) => song.id); for (const songId of allSongIds) { if (!playlistSongIds?.includes(songId)) { @@ -138,10 +146,12 @@ export const AddToPlaylistContextModal = ({ } if (values.skipDuplicates ? uniqueSongIds.length > 0 : allSongIds.length > 0) { + if (!server) return null; addToPlaylistMutation.mutate( { body: { songId: values.skipDuplicates ? uniqueSongIds : allSongIds }, query: { id: playlistId }, + serverId: server?.id, }, { onError: (err) => { diff --git a/src/renderer/features/playlists/components/create-playlist-form.tsx b/src/renderer/features/playlists/components/create-playlist-form.tsx index 45295d9d..dd9a3716 100644 --- a/src/renderer/features/playlists/components/create-playlist-form.tsx +++ b/src/renderer/features/playlists/components/create-playlist-form.tsx @@ -16,47 +16,54 @@ interface CreatePlaylistFormProps { } export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => { - const mutation = useCreatePlaylist(); + const mutation = useCreatePlaylist({}); const server = useCurrentServer(); const queryBuilderRef = useRef(null); const form = useForm({ initialValues: { + _custom: { + navidrome: { + public: false, + rules: undefined, + }, + }, comment: '', name: '', - ndParams: { - public: false, - rules: undefined, - }, }, }); const [isSmartPlaylist, setIsSmartPlaylist] = useState(false); const handleSubmit = form.onSubmit((values) => { if (isSmartPlaylist) { - values.ndParams = { - ...values.ndParams, + values._custom!.navidrome = { + ...values._custom?.navidrome, rules: queryBuilderRef.current?.getFilters(), }; } const smartPlaylist = queryBuilderRef.current?.getFilters(); + if (!server) return; + mutation.mutate( { body: { ...values, - ndParams: { - ...values.ndParams, - rules: - isSmartPlaylist && smartPlaylist?.filters - ? { - ...convertQueryGroupToNDQuery(smartPlaylist.filters), - ...smartPlaylist.extraFilters, - } - : undefined, + _custom: { + navidrome: { + ...values._custom?.navidrome, + rules: + isSmartPlaylist && smartPlaylist?.filters + ? { + ...convertQueryGroupToNDQuery(smartPlaylist.filters), + ...smartPlaylist.extraFilters, + } + : undefined, + }, }, }, + serverId: server.id, }, { onError: (err) => { diff --git a/src/renderer/features/playlists/components/playlist-detail-content.tsx b/src/renderer/features/playlists/components/playlist-detail-content.tsx index f734fc01..9de71c21 100644 --- a/src/renderer/features/playlists/components/playlist-detail-content.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-content.tsx @@ -17,16 +17,12 @@ import { UserListQuery, UserListSort, } from '/@/renderer/api/types'; +import { Button, ConfirmModal, DropdownMenu, MotionGroup, toast } from '/@/renderer/components'; import { - Button, - ConfirmModal, - DropdownMenu, getColumnDefs, - MotionGroup, - toast, useFixedTableHeader, VirtualTable, -} from '/@/renderer/components'; +} from '/@/renderer/components/virtual-table'; import { useHandleTableContextMenu } from '/@/renderer/features/context-menu'; import { PLAYLIST_SONG_CONTEXT_MENU_ITEMS, @@ -68,19 +64,23 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps) const { playlistId } = useParams() as { playlistId: string }; const page = useSongListStore(); const handlePlayQueueAdd = usePlayQueueAdd(); - const detailQuery = usePlaylistDetail({ id: playlistId }); + const server = useCurrentServer(); + const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); const playButtonBehavior = usePlayButtonBehavior(); const queryClient = useQueryClient(); - const server = useCurrentServer(); - const playlistSongsQueryInfinite = usePlaylistSongListInfinite( - { + const playlistSongsQueryInfinite = usePlaylistSongListInfinite({ + options: { + cacheTime: 0, + keepPreviousData: false, + }, + query: { id: playlistId, limit: 50, startIndex: 0, }, - { cacheTime: 0, keepPreviousData: false }, - ); + serverId: server?.id, + }); const handleLoadMore = () => { playlistSongsQueryInfinite.fetchNextPage(); @@ -105,17 +105,17 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps) }); const playlistSongData = useMemo( - () => playlistSongsQueryInfinite.data?.pages.flatMap((p) => p.items), + () => playlistSongsQueryInfinite.data?.pages.flatMap((p) => p?.items), [playlistSongsQueryInfinite.data?.pages], ); const { intersectRef, tableContainerRef } = useFixedTableHeader(); - const deletePlaylistMutation = useDeletePlaylist(); + const deletePlaylistMutation = useDeletePlaylist({}); const handleDeletePlaylist = () => { deletePlaylistMutation.mutate( - { query: { id: playlistId } }, + { query: { id: playlistId }, serverId: server?.id }, { onError: (err) => { toast.error({ @@ -165,30 +165,33 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps) startIndex: 0, }; + if (!server) return; + const users = await queryClient.fetchQuery({ - queryFn: ({ signal }) => api.controller.getUserList({ query, server, signal }), + queryFn: ({ signal }) => + api.controller.getUserList({ apiClientProps: { server, signal }, query }), queryKey: queryKeys.users.list(server?.id || '', query), }); - const normalizedUsers = api.normalize.userList(users, server); - openModal({ children: ( ), diff --git a/src/renderer/features/playlists/components/playlist-detail-header.tsx b/src/renderer/features/playlists/components/playlist-detail-header.tsx index 90fb9554..863dab62 100644 --- a/src/renderer/features/playlists/components/playlist-detail-header.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-header.tsx @@ -7,6 +7,7 @@ import { LibraryHeader } from '/@/renderer/features/shared'; import { AppRoute } from '/@/renderer/router/routes'; import { formatDurationString } from '/@/renderer/utils'; import { LibraryItem } from '/@/renderer/api/types'; +import { useCurrentServer } from '../../../store/auth.store'; interface PlaylistDetailHeaderProps { background: string; @@ -20,7 +21,8 @@ export const PlaylistDetailHeader = forwardRef( ref: Ref, ) => { const { playlistId } = useParams() as { playlistId: string }; - const detailQuery = usePlaylistDetail({ id: playlistId }); + const server = useCurrentServer(); + const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); const metadataItems = [ { 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 8f5c03f6..167fbe41 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 @@ -8,12 +8,6 @@ import type { RowDoubleClickedEvent, } from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; -import { - getColumnDefs, - TablePagination, - VirtualGridAutoSizerContainer, - VirtualTable, -} from '/@/renderer/components'; import { useCurrentServer, usePlaylistDetailStore, @@ -44,6 +38,8 @@ import { usePlayQueueAdd } from '/@/renderer/features/player'; import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query'; +import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid'; +import { getColumnDefs, VirtualTable, TablePagination } from '/@/renderer/components/virtual-table'; interface PlaylistDetailContentProps { tableRef: MutableRefObject; @@ -61,7 +57,7 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten }; }, [page?.table.id, playlistId]); - const detailQuery = usePlaylistDetail({ id: playlistId }); + const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); const p = usePlaylistDetailTablePagination(playlistId); const pagination = { @@ -80,9 +76,12 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED; const checkPlaylistList = usePlaylistSongList({ - id: playlistId, - limit: 1, - startIndex: 0, + query: { + id: playlistId, + limit: 1, + startIndex: 0, + }, + serverId: server?.id, }); const columnDefs: ColDef[] = useMemo( @@ -104,24 +103,27 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten ...filters, }); + if (!server) return; + const songsRes = await queryClient.fetchQuery( queryKey, async ({ signal }) => api.controller.getPlaylistSongList({ + apiClientProps: { + server, + signal, + }, query: { id: playlistId, limit, startIndex, ...filters, }, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); - const songs = api.normalize.songList(songsRes, server); - params.successCallback(songs?.items || [], songsRes?.totalRecordCount || 0); + params.successCallback(songsRes?.items || [], songsRes?.totalRecordCount || 0); }, rowCount: undefined, }; 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 7c559207..0232d23d 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 @@ -18,15 +18,7 @@ import { import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; import { LibraryItem, PlaylistSongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types'; -import { - DropdownMenu, - SONG_TABLE_COLUMNS, - Button, - Slider, - MultiSelect, - Switch, - Text, -} from '/@/renderer/components'; +import { DropdownMenu, Button, Slider, MultiSelect, Switch, Text } from '/@/renderer/components'; import { usePlayQueueAdd } from '/@/renderer/features/player'; import { useContainerQuery } from '/@/renderer/hooks'; import { @@ -41,6 +33,7 @@ import { import { ListDisplayType, ServerType, Play, TableColumn } from '/@/renderer/types'; import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query'; import { useParams } from 'react-router'; +import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; const FILTERS = { jellyfin: [ @@ -100,7 +93,7 @@ export const PlaylistDetailSongListHeaderFilters = ({ sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC, }; - const detailQuery = usePlaylistDetail({ id: playlistId }); + const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); const isSmartPlaylist = detailQuery.data?.rules; const handlePlayQueueAdd = usePlayQueueAdd(); @@ -139,14 +132,16 @@ export const PlaylistDetailSongListHeaderFilters = ({ queryKey, async ({ signal }) => api.controller.getPlaylistSongList({ + apiClientProps: { + server, + signal, + }, query: { id: playlistId, limit, startIndex, ...filters, }, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); 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 7bbc0479..ed52843c 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 @@ -8,6 +8,7 @@ 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 { LibraryHeaderBar } from '/@/renderer/features/shared'; +import { useCurrentServer } from '/@/renderer/store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { Play } from '/@/renderer/types'; @@ -23,7 +24,8 @@ export const PlaylistDetailSongListHeader = ({ handleToggleShowQueryBuilder, }: PlaylistDetailHeaderProps) => { const { playlistId } = useParams() as { playlistId: string }; - const detailQuery = usePlaylistDetail({ id: playlistId }); + const server = useCurrentServer(); + const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlay = async (playType: Play) => { diff --git a/src/renderer/features/playlists/components/playlist-list-content.tsx b/src/renderer/features/playlists/components/playlist-list-content.tsx index 43ab2ba3..d04f6a4e 100644 --- a/src/renderer/features/playlists/components/playlist-list-content.tsx +++ b/src/renderer/features/playlists/components/playlist-list-content.tsx @@ -12,12 +12,6 @@ import { Stack } from '@mantine/core'; import { useQueryClient } from '@tanstack/react-query'; import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; -import { - getColumnDefs, - TablePagination, - VirtualGridAutoSizerContainer, - VirtualTable, -} from '/@/renderer/components'; import { useCurrentServer, usePlaylistListStore, @@ -33,6 +27,8 @@ import { PLAYLIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/c import { generatePath, useNavigate } from 'react-router'; import { AppRoute } from '/@/renderer/router/routes'; import { LibraryItem } from '/@/renderer/api/types'; +import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid'; +import { getColumnDefs, VirtualTable, TablePagination } from '/@/renderer/components/virtual-table'; interface PlaylistListContentProps { itemCount?: number; @@ -81,13 +77,15 @@ export const PlaylistListContent = ({ tableRef, itemCount }: PlaylistListContent queryKey, async ({ signal }) => api.controller.getPlaylistList({ + apiClientProps: { + server, + signal, + }, query: { limit, startIndex, ...page.filter, }, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); diff --git a/src/renderer/features/playlists/components/playlist-list-header-filters.tsx b/src/renderer/features/playlists/components/playlist-list-header-filters.tsx index c8c41917..e01ffd4b 100644 --- a/src/renderer/features/playlists/components/playlist-list-header-filters.tsx +++ b/src/renderer/features/playlists/components/playlist-list-header-filters.tsx @@ -7,15 +7,7 @@ import { RiSortAsc, RiSortDesc, RiMoreFill, RiRefreshLine, RiSettings3Fill } fro import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; import { SortOrder, PlaylistListSort } from '/@/renderer/api/types'; -import { - DropdownMenu, - PLAYLIST_TABLE_COLUMNS, - Text, - Button, - Slider, - MultiSelect, - Switch, -} from '/@/renderer/components'; +import { DropdownMenu, Text, Button, Slider, MultiSelect, Switch } from '/@/renderer/components'; import { useContainerQuery } from '/@/renderer/hooks'; import { PlaylistListFilter, @@ -27,6 +19,7 @@ import { useSetPlaylistTablePagination, } from '/@/renderer/store'; import { ListDisplayType, TableColumn } from '/@/renderer/types'; +import { PLAYLIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; const FILTERS = { jellyfin: [ @@ -91,13 +84,15 @@ export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFilter queryKey, async ({ signal }) => api.controller.getPlaylistList({ + apiClientProps: { + server, + signal, + }, query: { limit, startIndex, ...pageFilters, }, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); diff --git a/src/renderer/features/playlists/components/save-as-playlist-form.tsx b/src/renderer/features/playlists/components/save-as-playlist-form.tsx index 6fa91808..81b3c909 100644 --- a/src/renderer/features/playlists/components/save-as-playlist-form.tsx +++ b/src/renderer/features/playlists/components/save-as-playlist-form.tsx @@ -1,6 +1,6 @@ import { Group, Stack } from '@mantine/core'; import { useForm } from '@mantine/form'; -import { CreatePlaylistBody, RawCreatePlaylistResponse, ServerType } from '/@/renderer/api/types'; +import { CreatePlaylistBody, CreatePlaylistResponse, ServerType } from '/@/renderer/api/types'; import { Button, Switch, TextInput, toast } from '/@/renderer/components'; import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation'; import { useCurrentServer } from '/@/renderer/store'; @@ -8,22 +8,24 @@ import { useCurrentServer } from '/@/renderer/store'; interface SaveAsPlaylistFormProps { body: Partial; onCancel: () => void; - onSuccess: (data: RawCreatePlaylistResponse) => void; + onSuccess: (data: CreatePlaylistResponse) => void; } export const SaveAsPlaylistForm = ({ body, onSuccess, onCancel }: SaveAsPlaylistFormProps) => { - const mutation = useCreatePlaylist(); + const mutation = useCreatePlaylist({}); const server = useCurrentServer(); const form = useForm({ initialValues: { + _custom: { + navidrome: { + public: false, + rules: undefined, + ...body?._custom?.navidrome, + }, + }, comment: body.comment || '', name: body.name || '', - ndParams: { - public: false, - rules: undefined, - ...body.ndParams, - }, }, }); diff --git a/src/renderer/features/playlists/components/update-playlist-form.tsx b/src/renderer/features/playlists/components/update-playlist-form.tsx index 2222ce80..deaefd16 100644 --- a/src/renderer/features/playlists/components/update-playlist-form.tsx +++ b/src/renderer/features/playlists/components/update-playlist-form.tsx @@ -13,7 +13,7 @@ interface UpdatePlaylistFormProps { } export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlaylistFormProps) => { - const mutation = useUpdatePlaylist(); + const mutation = useUpdatePlaylist({}); const server = useCurrentServer(); const userList = users?.map((user) => ({ @@ -23,21 +23,27 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl const form = useForm({ initialValues: { + _custom: { + navidrome: { + owner: body?._custom?.navidrome?.owner || '', + ownerId: body?._custom?.navidrome?.ownerId || '', + public: body?._custom?.navidrome?.public || false, + rules: undefined, + sync: body?._custom?.navidrome?.sync || false, + }, + }, comment: body?.comment || '', name: body?.name || '', - ndParams: { - owner: body?.ndParams?.owner || '', - ownerId: body?.ndParams?.ownerId || '', - public: body?.ndParams?.public || false, - rules: undefined, - sync: body?.ndParams?.sync || false, - }, }, }); const handleSubmit = form.onSubmit((values) => { mutation.mutate( - { body: values, query }, + { + body: values, + query, + serverId: server?.id, + }, { onError: (err) => { toast.error({ message: err.message, title: 'Error updating playlist' }); diff --git a/src/renderer/features/playlists/routes/playlist-detail-route.tsx b/src/renderer/features/playlists/routes/playlist-detail-route.tsx index a6233f73..c3090a01 100644 --- a/src/renderer/features/playlists/routes/playlist-detail-route.tsx +++ b/src/renderer/features/playlists/routes/playlist-detail-route.tsx @@ -1,5 +1,5 @@ -import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { useRef } from 'react'; +import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { useParams } from 'react-router'; import { LibraryItem } from '/@/renderer/api/types'; import { NativeScrollArea } from '/@/renderer/components'; @@ -10,14 +10,16 @@ import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playli import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared'; import { useFastAverageColor } from '/@/renderer/hooks'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; +import { useCurrentServer } from '../../../store/auth.store'; const PlaylistDetailRoute = () => { const tableRef = useRef(null); const scrollAreaRef = useRef(null); const headerRef = useRef(null); const { playlistId } = useParams() as { playlistId: string }; + const server = useCurrentServer(); - const detailQuery = usePlaylistDetail({ id: playlistId }); + const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); const background = useFastAverageColor( detailQuery?.data?.imageUrl, !detailQuery?.isLoading, 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 a3ac5196..4fa81d6e 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 @@ -23,11 +23,11 @@ const PlaylistDetailSongListRoute = () => { const navigate = useNavigate(); const tableRef = useRef(null); const { playlistId } = useParams() as { playlistId: string }; - const currentServer = useCurrentServer(); + const server = useCurrentServer(); - const detailQuery = usePlaylistDetail({ id: playlistId }); - const createPlaylistMutation = useCreatePlaylist(); - const deletePlaylistMutation = useDeletePlaylist(); + const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id }); + const createPlaylistMutation = useCreatePlaylist({}); + const deletePlaylistMutation = useDeletePlaylist({}); const handleSave = ( filter: Record, @@ -45,15 +45,17 @@ const PlaylistDetailSongListRoute = () => { createPlaylistMutation.mutate( { body: { + _custom: { + navidrome: { + owner: detailQuery?.data?.owner || '', + ownerId: detailQuery?.data?.ownerId || '', + public: detailQuery?.data?.public || false, + rules, + sync: detailQuery?.data?.sync || false, + }, + }, comment: detailQuery?.data?.description || '', name: detailQuery?.data?.name, - ndParams: { - owner: detailQuery?.data?.owner || '', - ownerId: detailQuery?.data?.ownerId || '', - public: detailQuery?.data?.public || false, - rules, - sync: detailQuery?.data?.sync || false, - }, }, }, { @@ -73,19 +75,21 @@ const PlaylistDetailSongListRoute = () => { children: ( @@ -120,9 +124,7 @@ const PlaylistDetailSongListRoute = () => { }; const isSmartPlaylist = - !detailQuery?.isLoading && - detailQuery?.data?.rules && - currentServer?.type === ServerType.NAVIDROME; + !detailQuery?.isLoading && detailQuery?.data?.rules && server?.type === ServerType.NAVIDROME; const [showQueryBuilder, setShowQueryBuilder] = useState(false); const [isQueryBuilderExpanded, setIsQueryBuilderExpanded] = useState(false); @@ -142,18 +144,19 @@ const PlaylistDetailSongListRoute = () => { sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC, }; - const itemCountCheck = usePlaylistSongList( - { + const itemCountCheck = usePlaylistSongList({ + options: { + cacheTime: 1000 * 60 * 60 * 2, + staleTime: 1000 * 60 * 60 * 2, + }, + query: { id: playlistId, limit: 1, startIndex: 0, ...filters, }, - { - cacheTime: 1000 * 60 * 60 * 2, - staleTime: 1000 * 60 * 60 * 2, - }, - ); + serverId: server?.id, + }); const itemCount = itemCountCheck.data?.totalRecordCount === null diff --git a/src/renderer/features/playlists/routes/playlist-list-route.tsx b/src/renderer/features/playlists/routes/playlist-list-route.tsx index 280140d6..0899a2fd 100644 --- a/src/renderer/features/playlists/routes/playlist-list-route.tsx +++ b/src/renderer/features/playlists/routes/playlist-list-route.tsx @@ -9,18 +9,18 @@ import { AnimatedPage } from '/@/renderer/features/shared'; const PlaylistListRoute = () => { const tableRef = useRef(null); - const itemCountCheck = usePlaylistList( - { + const itemCountCheck = usePlaylistList({ + options: { + cacheTime: 1000 * 60 * 60 * 2, + staleTime: 1000 * 60 * 60 * 2, + }, + query: { limit: 1, sortBy: PlaylistListSort.NAME, sortOrder: SortOrder.ASC, startIndex: 0, }, - { - cacheTime: 1000 * 60 * 60 * 2, - staleTime: 1000 * 60 * 60 * 2, - }, - ); + }); const itemCount = itemCountCheck.data?.totalRecordCount === null diff --git a/src/renderer/features/servers/components/add-server-form.tsx b/src/renderer/features/servers/components/add-server-form.tsx index 48cf5bed..80e9a481 100644 --- a/src/renderer/features/servers/components/add-server-form.tsx +++ b/src/renderer/features/servers/components/add-server-form.tsx @@ -77,7 +77,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => { setCurrentServer(serverItem); closeAllModals(); - if (serverList.length === 0) { + if (Object.keys(serverList).length === 0) { toast.success({ message: 'Server has been added, reloading...' }); setTimeout(() => window.location.reload(), 2000); } else { diff --git a/src/renderer/features/servers/components/server-list.tsx b/src/renderer/features/servers/components/server-list.tsx index 4d08d743..fd8d38c6 100644 --- a/src/renderer/features/servers/components/server-list.tsx +++ b/src/renderer/features/servers/components/server-list.tsx @@ -62,6 +62,7 @@ export const ServerList = () => { position: 'absolute', right: 55, transform: 'translateY(-3.5rem)', + zIndex: 2000, }} > - {musicFoldersQuery.data?.map((folder) => ( + {musicFoldersQuery.data?.items.map((folder) => ( api.controller.getSongList({ + apiClientProps: { + server, + signal, + }, query, - server, - signal, }), { cacheTime: 1000 * 60 * 1 }, ); - const songs = api.normalize.songList(songsRes, server); - params.successCallback(songs?.items || [], songsRes?.totalRecordCount || 0); + params.successCallback(songsRes?.items || [], songsRes?.totalRecordCount || 0); }, rowCount: undefined, }; diff --git a/src/renderer/features/songs/routes/song-list-route.tsx b/src/renderer/features/songs/routes/song-list-route.tsx index f1e84156..afe323cc 100644 --- a/src/renderer/features/songs/routes/song-list-route.tsx +++ b/src/renderer/features/songs/routes/song-list-route.tsx @@ -19,17 +19,18 @@ const TrackListRoute = () => { ); const songListFilter = useSongListFilter({ id: albumArtistId, key: pageKey }); - const itemCountCheck = useSongList( - { + const itemCountCheck = useSongList({ + options: { + cacheTime: 1000 * 60, + staleTime: 1000 * 60, + }, + query: { limit: 1, startIndex: 0, ...songListFilter, }, - { - cacheTime: 1000 * 60, - staleTime: 1000 * 60, - }, - ); + serverId: server?.id, + }); const itemCount = itemCountCheck.data?.totalRecordCount === null diff --git a/src/renderer/features/titlebar/components/app-menu.tsx b/src/renderer/features/titlebar/components/app-menu.tsx index a19f478a..b908c4f5 100644 --- a/src/renderer/features/titlebar/components/app-menu.tsx +++ b/src/renderer/features/titlebar/components/app-menu.tsx @@ -60,22 +60,23 @@ export const AppMenu = () => { return ( <> Select a server - {serverList.map((s) => { - const isNavidromeExpired = s.type === ServerType.NAVIDROME && !s.ndCredential; + {Object.keys(serverList).map((serverId) => { + const server = serverList[serverId]; + const isNavidromeExpired = server.type === ServerType.NAVIDROME && !server.ndCredential; const isJellyfinExpired = false; const isSessionExpired = isNavidromeExpired || isJellyfinExpired; return ( : } onClick={() => { - if (!isSessionExpired) return handleSetCurrentServer(s); - return handleCredentialsModal(s); + if (!isSessionExpired) return handleSetCurrentServer(server); + return handleCredentialsModal(server); }} > - {s.name} + {server.name} ); })}