Refactor all api instances in components

This commit is contained in:
jeffvli 2023-04-30 22:01:52 -07:00
parent bdd023fde3
commit 314bd766df
56 changed files with 879 additions and 755 deletions

View file

@ -27,8 +27,6 @@ export * from './text';
export * from './text-title'; export * from './text-title';
export * from './toast'; export * from './toast';
export * from './tooltip'; export * from './tooltip';
export * from './virtual-grid';
export * from './virtual-table';
export * from './motion'; export * from './motion';
export * from './context-menu'; export * from './context-menu';
export * from './query-builder'; export * from './query-builder';

View file

@ -1,50 +1,13 @@
/* eslint-disable import/no-cycle */
import type { ICellRendererParams } from '@ag-grid-community/core'; import type { ICellRendererParams } from '@ag-grid-community/core';
import { RiHeartFill, RiHeartLine } from 'react-icons/ri'; import { RiHeartFill, RiHeartLine } from 'react-icons/ri';
import { Button } from '/@/renderer/components/button'; import { Button } from '/@/renderer/components/button';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell'; import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
import { useMutation } from '@tanstack/react-query'; import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
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<RawFavoriteResponse, HTTPError, Omit<FavoriteArgs, 'server'>, 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<RawFavoriteResponse, HTTPError, Omit<FavoriteArgs, 'server'>, 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 });
}
}
},
});
};
export const FavoriteCell = ({ value, data, node }: ICellRendererParams) => { export const FavoriteCell = ({ value, data, node }: ICellRendererParams) => {
const createMutation = useCreateFavorite(); const createMutation = useCreateFavorite({});
const deleteMutation = useDeleteFavorite(); const deleteMutation = useDeleteFavorite({});
const handleToggleFavorite = () => { const handleToggleFavorite = () => {
const newFavoriteValue = !value; const newFavoriteValue = !value;

View file

@ -1,22 +1,23 @@
/* eslint-disable import/no-cycle */
import { MouseEvent } from 'react'; import { MouseEvent } from 'react';
import type { ICellRendererParams } from '@ag-grid-community/core'; import type { ICellRendererParams } from '@ag-grid-community/core';
import { Rating } from '/@/renderer/components/rating'; import { Rating } from '/@/renderer/components/rating';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell'; 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) => { export const RatingCell = ({ value, node }: ICellRendererParams) => {
const updateRatingMutation = useUpdateRating(); const updateRatingMutation = useSetRating({});
const handleUpdateRating = (rating: number) => { const handleUpdateRating = (rating: number) => {
if (!value) return; if (!value) return;
updateRatingMutation.mutate( updateRatingMutation.mutate(
{ {
_serverId: value?.serverId,
query: { query: {
item: [value], item: [value],
rating, rating,
}, },
serverId: value?.serverId,
}, },
{ {
onSuccess: () => { onSuccess: () => {
@ -31,11 +32,11 @@ export const RatingCell = ({ value, node }: ICellRendererParams) => {
e.stopPropagation(); e.stopPropagation();
updateRatingMutation.mutate( updateRatingMutation.mutate(
{ {
_serverId: value?.serverId,
query: { query: {
item: [value], item: [value],
rating: 0, rating: 0,
}, },
serverId: value?.serverId,
}, },
{ {
onSuccess: () => { onSuccess: () => {

View file

@ -1,3 +1,4 @@
/* eslint-disable import/no-cycle */
import { Ref, forwardRef, useRef, useEffect, useCallback, useMemo } from 'react'; import { Ref, forwardRef, useRef, useEffect, useCallback, useMemo } from 'react';
import type { import type {
ICellRendererParams, ICellRendererParams,
@ -28,8 +29,8 @@ import { GenericTableHeader } from '/@/renderer/components/virtual-table/headers
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { PersistedTableColumn } from '/@/renderer/store/settings.store'; import { PersistedTableColumn } from '/@/renderer/store/settings.store';
import { TableColumn } from '/@/renderer/types'; 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 { 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-config-dropdown';
export * from './table-pagination'; export * from './table-pagination';

View file

@ -1,13 +1,5 @@
import { MutableRefObject, useCallback, useMemo } from 'react'; import { MutableRefObject, useCallback, useMemo } from 'react';
import { import { Button, GridCarousel, Text, TextTitle } from '/@/renderer/components';
Button,
getColumnDefs,
GridCarousel,
Text,
TextTitle,
useFixedTableHeader,
VirtualTable,
} from '/@/renderer/components';
import { ColDef, RowDoubleClickedEvent, RowHeightParams, RowNode } from '@ag-grid-community/core'; import { ColDef, RowDoubleClickedEvent, RowHeightParams, RowNode } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Box, Group, Stack } from '@mantine/core'; 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 { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
import { AlbumListSort, LibraryItem, QueueSong, SortOrder } from '/@/renderer/api/types'; import { AlbumListSort, LibraryItem, QueueSong, SortOrder } from '/@/renderer/api/types';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { useCurrentServer } from '/@/renderer/store';
import {
getColumnDefs,
useFixedTableHeader,
VirtualTable,
} from '/@/renderer/components/virtual-table';
const isFullWidthRow = (node: RowNode) => { const isFullWidthRow = (node: RowNode) => {
return node.id?.includes('disc-'); return node.id?.includes('disc-');
@ -60,7 +58,8 @@ interface AlbumDetailContentProps {
export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => { export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => {
const { albumId } = useParams() as { albumId: string }; 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 cq = useContainerQuery();
const handlePlayQueueAdd = usePlayQueueAdd(); 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 itemsPerPage = cq.isXl ? 9 : cq.isLg ? 7 : cq.isMd ? 5 : cq.isSm ? 4 : 3;
const artistQuery = useAlbumList( const artistQuery = useAlbumList({
{ options: {
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,
},
{
cacheTime: 1000 * 60, cacheTime: 1000 * 60,
enabled: detailQuery?.data?.albumArtists[0]?.id !== undefined, enabled: detailQuery?.data?.albumArtists[0]?.id !== undefined,
keepPreviousData: true, keepPreviousData: true,
staleTime: 1000 * 60, 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 = [ const carousels = [
{ {
@ -227,8 +229,8 @@ export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => {
}); });
}; };
const createFavoriteMutation = useCreateFavorite(); const createFavoriteMutation = useCreateFavorite({});
const deleteFavoriteMutation = useDeleteFavorite(); const deleteFavoriteMutation = useDeleteFavorite({});
const handleFavorite = () => { const handleFavorite = () => {
if (!detailQuery?.data) return; if (!detailQuery?.data) return;

View file

@ -5,9 +5,10 @@ import { Link } from 'react-router-dom';
import { LibraryItem, ServerType } from '/@/renderer/api/types'; import { LibraryItem, ServerType } from '/@/renderer/api/types';
import { Button, Rating, Text } from '/@/renderer/components'; import { Button, Rating, Text } from '/@/renderer/components';
import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query'; 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 { useContainerQuery } from '/@/renderer/hooks';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { formatDurationString } from '/@/renderer/utils'; import { formatDurationString } from '/@/renderer/utils';
interface AlbumDetailHeaderProps { interface AlbumDetailHeaderProps {
@ -17,7 +18,8 @@ interface AlbumDetailHeaderProps {
export const AlbumDetailHeader = forwardRef( export const AlbumDetailHeader = forwardRef(
({ background }: AlbumDetailHeaderProps, ref: Ref<HTMLDivElement>) => { ({ background }: AlbumDetailHeaderProps, ref: Ref<HTMLDivElement>) => {
const { albumId } = useParams() as { albumId: string }; 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 cq = useContainerQuery();
const metadataItems = [ const metadataItems = [
@ -38,17 +40,17 @@ export const AlbumDetailHeader = forwardRef(
}, },
]; ];
const updateRatingMutation = useUpdateRating(); const updateRatingMutation = useSetRating({});
const handleUpdateRating = (rating: number) => { const handleUpdateRating = (rating: number) => {
if (!detailQuery?.data) return; if (!detailQuery?.data) return;
updateRatingMutation.mutate({ updateRatingMutation.mutate({
_serverId: detailQuery?.data.serverId,
query: { query: {
item: [detailQuery.data], item: [detailQuery.data],
rating, rating,
}, },
serverId: detailQuery.data.serverId,
}); });
}; };
@ -56,11 +58,11 @@ export const AlbumDetailHeader = forwardRef(
if (!detailQuery?.data || !detailQuery?.data.userRating) return; if (!detailQuery?.data || !detailQuery?.data.userRating) return;
updateRatingMutation.mutate({ updateRatingMutation.mutate({
_serverId: detailQuery.data.serverId,
query: { query: {
item: [detailQuery.data], item: [detailQuery.data],
rating: 0, rating: 0,
}, },
serverId: detailQuery.data.serverId,
}); });
}; };

View file

@ -1,12 +1,4 @@
import { import { ALBUM_CARD_ROWS } from '/@/renderer/components';
ALBUM_CARD_ROWS,
getColumnDefs,
TablePagination,
VirtualGridAutoSizerContainer,
VirtualInfiniteGrid,
VirtualInfiniteGridRef,
VirtualTable,
} from '/@/renderer/components';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { ListDisplayType, CardRow } from '/@/renderer/types'; import { ListDisplayType, CardRow } from '/@/renderer/types';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
@ -40,6 +32,12 @@ import { generatePath, useNavigate } from 'react-router';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared'; import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; 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 { interface AlbumListContentProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>; gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
@ -71,29 +69,36 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
limit, limit,
startIndex, startIndex,
...filter, ...filter,
jfParams: { _custom: {
...filter.jfParams, jellyfin: {
...filter._custom?.jellyfin,
},
navidrome: {
...filter._custom?.navidrome,
}, },
ndParams: {
...filter.ndParams,
}, },
}; };
const queryKey = queryKeys.albums.list(server?.id || '', query); const queryKey = queryKeys.albums.list(server?.id || '', query);
if (!server) {
return params.failCallback();
}
const albumsRes = await queryClient.fetchQuery( const albumsRes = await queryClient.fetchQuery(
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getAlbumList({ api.controller.getAlbumList({
query, apiClientProps: {
server, server,
signal, signal,
},
query,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );
const albums = api.normalize.albumList(albumsRes, server); return params.successCallback(albumsRes?.items || [], albumsRes?.totalRecordCount || 0);
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || 0);
}, },
rowCount: undefined, rowCount: undefined,
}; };
@ -165,15 +170,21 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
const fetch = useCallback( const fetch = useCallback(
async ({ skip, take }: { skip: number; take: number }) => { async ({ skip, take }: { skip: number; take: number }) => {
if (!server) {
return [];
}
const query: AlbumListQuery = { const query: AlbumListQuery = {
limit: take, limit: take,
startIndex: skip, startIndex: skip,
...filter, ...filter,
jfParams: { _custom: {
...filter.jfParams, jellyfin: {
...filter._custom?.jellyfin,
},
navidrome: {
...filter._custom?.navidrome,
}, },
ndParams: {
...filter.ndParams,
}, },
}; };
@ -181,13 +192,15 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) => const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) =>
controller.getAlbumList({ controller.getAlbumList({
query, apiClientProps: {
server, server,
signal, signal,
},
query,
}), }),
); );
return api.normalize.albumList(albums, server); return albums;
}, },
[filter, queryClient, server], [filter, queryClient, server],
); );
@ -268,8 +281,8 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id })); navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id }));
}; };
const createFavoriteMutation = useCreateFavorite(); const createFavoriteMutation = useCreateFavorite({});
const deleteFavoriteMutation = useDeleteFavorite(); const deleteFavoriteMutation = useDeleteFavorite({});
const handleFavorite = (options: { const handleFavorite = (options: {
id: string[]; id: string[];

View file

@ -20,16 +20,7 @@ import {
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumListQuery, AlbumListSort, LibraryItem, SortOrder } from '/@/renderer/api/types'; import { AlbumListQuery, AlbumListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
import { import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
ALBUM_TABLE_COLUMNS,
Button,
DropdownMenu,
MultiSelect,
Slider,
Switch,
Text,
VirtualInfiniteGridRef,
} from '/@/renderer/components';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { import {
AlbumListFilter, AlbumListFilter,
@ -43,6 +34,8 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters'; import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters';
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters'; import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; 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 = { const FILTERS = {
jellyfin: [ jellyfin: [
@ -100,7 +93,7 @@ export const AlbumListHeaderFilters = ({
const { display, filter, table, grid } = useAlbumListStore({ id, key: pageKey }); const { display, filter, table, grid } = useAlbumListStore({ id, key: pageKey });
const cq = useContainerQuery(); const cq = useContainerQuery();
const musicFoldersQuery = useMusicFolders(); const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id });
const sortByLabel = const sortByLabel =
(server?.type && (server?.type &&
@ -115,13 +108,15 @@ export const AlbumListHeaderFilters = ({
limit: take, limit: take,
startIndex: skip, startIndex: skip,
...filters, ...filters,
jfParams: { _custom: {
...filters.jfParams, jellyfin: {
...customFilters?.jfParams, ...filters._custom?.jellyfin,
...customFilters?._custom?.jellyfin,
},
navidrome: {
...filters._custom?.navidrome,
...customFilters?._custom?.navidrome,
}, },
ndParams: {
...filters.ndParams,
...customFilters?.ndParams,
}, },
...customFilters, ...customFilters,
}; };
@ -132,14 +127,16 @@ export const AlbumListHeaderFilters = ({
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getAlbumList({ api.controller.getAlbumList({
query, apiClientProps: {
server, server,
signal, signal,
},
query,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );
return api.normalize.albumList(albums, server); return albums;
}, },
[customFilters, queryClient, server], [customFilters, queryClient, server],
); );
@ -157,13 +154,15 @@ export const AlbumListHeaderFilters = ({
startIndex, startIndex,
...filters, ...filters,
...customFilters, ...customFilters,
jfParams: { _custom: {
...filters.jfParams, jellyfin: {
...customFilters?.jfParams, ...filters._custom?.jellyfin,
...customFilters?._custom?.jellyfin,
},
navidrome: {
...filters._custom?.navidrome,
...customFilters?._custom?.navidrome,
}, },
ndParams: {
...filters.ndParams,
...customFilters?.ndParams,
}, },
}; };
@ -173,15 +172,16 @@ export const AlbumListHeaderFilters = ({
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getAlbumList({ api.controller.getAlbumList({
query, apiClientProps: {
server, server,
signal, signal,
},
query,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );
const albums = api.normalize.albumList(albumsRes, server); return params.successCallback(albumsRes?.items || [], albumsRes?.totalRecordCount || 0);
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || 0);
}, },
rowCount: undefined, rowCount: undefined,
}; };
@ -218,6 +218,7 @@ export const AlbumListHeaderFilters = ({
handleFilterChange={handleFilterChange} handleFilterChange={handleFilterChange}
id={id} id={id}
pageKey={pageKey} pageKey={pageKey}
serverId={server?.id}
/> />
) : ( ) : (
<JellyfinAlbumFilters <JellyfinAlbumFilters
@ -225,6 +226,7 @@ export const AlbumListHeaderFilters = ({
handleFilterChange={handleFilterChange} handleFilterChange={handleFilterChange}
id={id} id={id}
pageKey={pageKey} pageKey={pageKey}
serverId={server?.id}
/> />
)} )}
</> </>
@ -293,30 +295,32 @@ export const AlbumListHeaderFilters = ({
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
const handlePlay = async (play: Play) => { const handlePlay = async (play: Play) => {
if (!itemCount || itemCount === 0) return; if (!itemCount || itemCount === 0 || !server) return;
const query = { const query = {
startIndex: 0, startIndex: 0,
...filter, ...filter,
...customFilters, ...customFilters,
jfParams: { _custom: {
...filter.jfParams, jellyfin: {
...customFilters?.jfParams, ...filter._custom?.jellyfin,
...customFilters?._custom?.jellyfin,
},
navidrome: {
...filter._custom?.navidrome,
...customFilters?._custom?.navidrome,
}, },
ndParams: {
...filter.ndParams,
...customFilters?.ndParams,
}, },
}; };
const queryKey = queryKeys.albums.list(server?.id || '', query); const queryKey = queryKeys.albums.list(server?.id || '', query);
const albumListRes = await queryClient.fetchQuery({ const albumListRes = await queryClient.fetchQuery({
queryFn: ({ signal }) => api.controller.getAlbumList({ query, server, signal }), queryFn: ({ signal }) =>
api.controller.getAlbumList({ apiClientProps: { server, signal }, query }),
queryKey, queryKey,
}); });
const albumIds = const albumIds = albumListRes?.items?.map((a) => a.id) || [];
api.normalize.albumList(albumListRes, server).items?.map((item) => item.id) || [];
handlePlayQueueAdd?.({ handlePlayQueueAdd?.({
byItemType: { byItemType: {
@ -382,16 +386,16 @@ export const AlbumListHeaderFilters = ({
const isFilterApplied = useMemo(() => { const isFilterApplied = useMemo(() => {
const isNavidromeFilterApplied = const isNavidromeFilterApplied =
server?.type === ServerType.NAVIDROME && server?.type === ServerType.NAVIDROME &&
filter.ndParams && filter?._custom.navidrome &&
Object.values(filter.ndParams).some((value) => value !== undefined); Object.values(filter._custom.navidrome).some((value) => value !== undefined);
const isJellyfinFilterApplied = const isJellyfinFilterApplied =
server?.type === ServerType.JELLYFIN && server?.type === ServerType.JELLYFIN &&
filter.jfParams && filter?._custom.jellyfin &&
Object.values(filter.jfParams).some((value) => value !== undefined); Object.values(filter._custom.jellyfin).some((value) => value !== undefined);
return isNavidromeFilterApplied || isJellyfinFilterApplied; return isNavidromeFilterApplied || isJellyfinFilterApplied;
}, [filter.jfParams, filter.ndParams, server?.type]); }, [filter._custom.jellyfin, filter._custom.navidrome, server?.type]);
return ( return (
<Flex justify="space-between"> <Flex justify="space-between">
@ -456,7 +460,7 @@ export const AlbumListHeaderFilters = ({
</Button> </Button>
</DropdownMenu.Target> </DropdownMenu.Target>
<DropdownMenu.Dropdown> <DropdownMenu.Dropdown>
{musicFoldersQuery.data?.map((folder) => ( {musicFoldersQuery.data?.items.map((folder) => (
<DropdownMenu.Item <DropdownMenu.Item
key={`musicFolder-${folder.id}`} key={`musicFolder-${folder.id}`}
$isActive={filter.musicFolderId === folder.id} $isActive={filter.musicFolderId === folder.id}

View file

@ -9,7 +9,7 @@ import { api } from '/@/renderer/api';
import { controller } from '/@/renderer/api/controller'; import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumListQuery, LibraryItem } from '/@/renderer/api/types'; import { AlbumListQuery, LibraryItem } from '/@/renderer/api/types';
import { PageHeader, SearchInput, VirtualInfiniteGridRef } from '/@/renderer/components'; import { PageHeader, SearchInput } from '/@/renderer/components';
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared'; import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { import {
@ -24,6 +24,7 @@ import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/a
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
interface AlbumListHeaderProps { interface AlbumListHeaderProps {
customFilters?: Partial<AlbumListFilter>; customFilters?: Partial<AlbumListFilter>;
@ -54,15 +55,17 @@ export const AlbumListHeader = ({
limit: take, limit: take,
startIndex: skip, startIndex: skip,
...filters, ...filters,
jfParams: {
...filters.jfParams,
...customFilters?.jfParams,
},
ndParams: {
...filters.ndParams,
...customFilters?.ndParams,
},
...customFilters, ...customFilters,
_custom: {
jellyfin: {
...filters._custom?.jellyfin,
...customFilters?._custom?.jellyfin,
},
navidrome: {
...filters._custom?.navidrome,
...customFilters?._custom?.navidrome,
},
},
}; };
const queryKey = queryKeys.albums.list(server?.id || '', query); const queryKey = queryKeys.albums.list(server?.id || '', query);
@ -71,14 +74,16 @@ export const AlbumListHeader = ({
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
controller.getAlbumList({ controller.getAlbumList({
query, apiClientProps: {
server, server,
signal, signal,
},
query,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );
return api.normalize.albumList(albums, server); return albums;
}, },
[customFilters, queryClient, server], [customFilters, queryClient, server],
); );
@ -96,13 +101,15 @@ export const AlbumListHeader = ({
startIndex, startIndex,
...filters, ...filters,
...customFilters, ...customFilters,
jfParams: { _custom: {
...filters.jfParams, jellyfin: {
...customFilters?.jfParams, ...filters._custom?.jellyfin,
...customFilters?._custom?.jellyfin,
},
navidrome: {
...filters._custom?.navidrome,
...customFilters?._custom?.navidrome,
}, },
ndParams: {
...filters.ndParams,
...customFilters?.ndParams,
}, },
}; };
@ -112,15 +119,16 @@ export const AlbumListHeader = ({
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getAlbumList({ api.controller.getAlbumList({
query, apiClientProps: {
server, server,
signal, signal,
},
query,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );
const albums = api.normalize.albumList(albumsRes, server); params.successCallback(albumsRes?.items || [], albumsRes?.totalRecordCount || 0);
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || 0);
}, },
rowCount: undefined, rowCount: undefined,
}; };
@ -164,24 +172,26 @@ export const AlbumListHeader = ({
startIndex: 0, startIndex: 0,
...filter, ...filter,
...customFilters, ...customFilters,
jfParams: { _custom: {
...filter.jfParams, jellyfin: {
...customFilters?.jfParams, ...filter._custom?.jellyfin,
...customFilters?._custom?.jellyfin,
},
navidrome: {
...filter._custom?.navidrome,
...customFilters?._custom?.navidrome,
}, },
ndParams: {
...filter.ndParams,
...customFilters?.ndParams,
}, },
}; };
const queryKey = queryKeys.albums.list(server?.id || '', query); const queryKey = queryKeys.albums.list(server?.id || '', query);
const albumListRes = await queryClient.fetchQuery({ const albumListRes = await queryClient.fetchQuery({
queryFn: ({ signal }) => api.controller.getAlbumList({ query, server, signal }), queryFn: ({ signal }) =>
api.controller.getAlbumList({ apiClientProps: { server, signal }, query }),
queryKey, queryKey,
}); });
const albumIds = const albumIds = albumListRes?.items?.map((item) => item.id) || [];
api.normalize.albumList(albumListRes, server).items?.map((item) => item.id) || [];
handlePlayQueueAdd?.({ handlePlayQueueAdd?.({
byItemType: { byItemType: {

View file

@ -12,6 +12,7 @@ interface JellyfinAlbumFiltersProps {
handleFilterChange: (filters: AlbumListFilter) => void; handleFilterChange: (filters: AlbumListFilter) => void;
id?: string; id?: string;
pageKey: string; pageKey: string;
serverId?: string;
} }
export const JellyfinAlbumFilters = ({ export const JellyfinAlbumFilters = ({
@ -19,24 +20,25 @@ export const JellyfinAlbumFilters = ({
handleFilterChange, handleFilterChange,
pageKey, pageKey,
id, id,
serverId,
}: JellyfinAlbumFiltersProps) => { }: JellyfinAlbumFiltersProps) => {
const filter = useAlbumListFilter({ id, key: pageKey }); const filter = useAlbumListFilter({ id, key: pageKey });
const { setFilter } = useListStoreActions(); const { setFilter } = useListStoreActions();
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library // 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(() => { const genreList = useMemo(() => {
if (!genreListQuery?.data) return []; if (!genreListQuery?.data) return [];
return genreListQuery.data.map((genre) => ({ return genreListQuery.data.items.map((genre) => ({
label: genre.name, label: genre.name,
value: genre.id, value: genre.id,
})); }));
}, [genreListQuery.data]); }, [genreListQuery.data]);
const selectedGenres = useMemo(() => { const selectedGenres = useMemo(() => {
return filter.jfParams?.genreIds?.split(','); return filter._custom?.jellyfin?.genreIds?.split(',');
}, [filter.jfParams?.genreIds]); }, [filter._custom?.jellyfin?.genreIds]);
const toggleFilters = [ const toggleFilters = [
{ {
@ -44,17 +46,19 @@ export const JellyfinAlbumFilters = ({
onChange: (e: ChangeEvent<HTMLInputElement>) => { onChange: (e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
jfParams: { _custom: {
...filter.jfParams, ...filter._custom,
includeItemTypes: 'Audio', jellyfin: {
...filter._custom?.jellyfin,
isFavorite: e.currentTarget.checked ? true : undefined, isFavorite: e.currentTarget.checked ? true : undefined,
}, },
}, },
},
key: pageKey, key: pageKey,
}) as AlbumListFilter; }) as AlbumListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
}, },
value: filter.jfParams?.isFavorite, value: filter._custom?.jellyfin?.isFavorite,
}, },
]; ];
@ -62,11 +66,14 @@ export const JellyfinAlbumFilters = ({
if (typeof e === 'number' && (e < 1700 || e > 2300)) return; if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
jfParams: { _custom: {
...filter.jfParams, ...filter._custom,
jellyfin: {
...filter._custom?.jellyfin,
minYear: e === '' ? undefined : (e as number), minYear: e === '' ? undefined : (e as number),
}, },
}, },
},
key: pageKey, key: pageKey,
}) as AlbumListFilter; }) as AlbumListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
@ -76,11 +83,14 @@ export const JellyfinAlbumFilters = ({
if (typeof e === 'number' && (e < 1700 || e > 2300)) return; if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
jfParams: { _custom: {
...filter.jfParams, ...filter._custom,
jellyfin: {
...filter._custom?.jellyfin,
maxYear: e === '' ? undefined : (e as number), maxYear: e === '' ? undefined : (e as number),
}, },
}, },
},
key: pageKey, key: pageKey,
}) as AlbumListFilter; }) as AlbumListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
@ -90,11 +100,14 @@ export const JellyfinAlbumFilters = ({
const genreFilterString = e?.length ? e.join(',') : undefined; const genreFilterString = e?.length ? e.join(',') : undefined;
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
jfParams: { _custom: {
...filter.jfParams, ...filter._custom,
jellyfin: {
...filter._custom?.jellyfin,
genreIds: genreFilterString, genreIds: genreFilterString,
}, },
}, },
},
key: pageKey, key: pageKey,
}) as AlbumListFilter; }) as AlbumListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
@ -102,17 +115,18 @@ export const JellyfinAlbumFilters = ({
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>(''); const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
const albumArtistListQuery = useAlbumArtistList( const albumArtistListQuery = useAlbumArtistList({
{ options: {
cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: {
sortBy: AlbumArtistListSort.NAME, sortBy: AlbumArtistListSort.NAME,
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
startIndex: 0, startIndex: 0,
}, },
{ serverId,
cacheTime: 1000 * 60 * 2, });
staleTime: 1000 * 60 * 1,
},
);
const selectableAlbumArtists = useMemo(() => { const selectableAlbumArtists = useMemo(() => {
if (!albumArtistListQuery?.data?.items) return []; if (!albumArtistListQuery?.data?.items) return [];
@ -127,11 +141,14 @@ export const JellyfinAlbumFilters = ({
const albumArtistFilterString = e?.length ? e.join(',') : undefined; const albumArtistFilterString = e?.length ? e.join(',') : undefined;
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
jfParams: { _custom: {
...filter.jfParams, ...filter._custom,
jellyfin: {
...filter._custom?.jellyfin,
albumArtistIds: albumArtistFilterString, albumArtistIds: albumArtistFilterString,
}, },
}, },
},
key: pageKey, key: pageKey,
}) as AlbumListFilter; }) as AlbumListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
@ -155,21 +172,21 @@ export const JellyfinAlbumFilters = ({
<Divider my="0.5rem" /> <Divider my="0.5rem" />
<Group grow> <Group grow>
<NumberInput <NumberInput
defaultValue={filter.jfParams?.minYear} defaultValue={filter._custom?.jellyfin?.minYear}
hideControls={false} hideControls={false}
label="From year" label="From year"
max={2300} max={2300}
min={1700} min={1700}
required={!!filter.jfParams?.maxYear} required={!!filter._custom?.jellyfin?.maxYear}
onChange={(e) => handleMinYearFilter(e)} onChange={(e) => handleMinYearFilter(e)}
/> />
<NumberInput <NumberInput
defaultValue={filter.jfParams?.maxYear} defaultValue={filter._custom?.jellyfin?.maxYear}
hideControls={false} hideControls={false}
label="To year" label="To year"
max={2300} max={2300}
min={1700} min={1700}
required={!!filter.jfParams?.minYear} required={!!filter._custom?.jellyfin?.minYear}
onChange={(e) => handleMaxYearFilter(e)} onChange={(e) => handleMaxYearFilter(e)}
/> />
</Group> </Group>
@ -189,7 +206,7 @@ export const JellyfinAlbumFilters = ({
clearable clearable
searchable searchable
data={selectableAlbumArtists} data={selectableAlbumArtists}
defaultValue={filter.jfParams?.albumArtistIds?.split(',')} defaultValue={filter._custom?.jellyfin?.albumArtistIds?.split(',')}
disabled={disableArtistFilter} disabled={disableArtistFilter}
label="Artist" label="Artist"
limit={300} limit={300}

View file

@ -12,6 +12,7 @@ interface NavidromeAlbumFiltersProps {
handleFilterChange: (filters: AlbumListFilter) => void; handleFilterChange: (filters: AlbumListFilter) => void;
id?: string; id?: string;
pageKey: string; pageKey: string;
serverId?: string;
} }
export const NavidromeAlbumFilters = ({ export const NavidromeAlbumFilters = ({
@ -19,15 +20,16 @@ export const NavidromeAlbumFilters = ({
disableArtistFilter, disableArtistFilter,
pageKey, pageKey,
id, id,
serverId,
}: NavidromeAlbumFiltersProps) => { }: NavidromeAlbumFiltersProps) => {
const filter = useAlbumListFilter({ id, key: pageKey }); const filter = useAlbumListFilter({ id, key: pageKey });
const { setFilter } = useListStoreActions(); const { setFilter } = useListStoreActions();
const genreListQuery = useGenreList(null); const genreListQuery = useGenreList({ query: null, serverId });
const genreList = useMemo(() => { const genreList = useMemo(() => {
if (!genreListQuery?.data) return []; if (!genreListQuery?.data) return [];
return genreListQuery.data.map((genre) => ({ return genreListQuery.data.items.map((genre) => ({
label: genre.name, label: genre.name,
value: genre.id, value: genre.id,
})); }));
@ -36,11 +38,14 @@ export const NavidromeAlbumFilters = ({
const handleGenresFilter = debounce((e: string | null) => { const handleGenresFilter = debounce((e: string | null) => {
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
ndParams: { _custom: {
...filter.ndParams, ...filter._custom,
navidrome: {
...filter._custom?.navidrome,
genre_id: e || undefined, genre_id: e || undefined,
}, },
}, },
},
key: 'album', key: 'album',
}) as AlbumListFilter; }) as AlbumListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
@ -52,71 +57,90 @@ export const NavidromeAlbumFilters = ({
onChange: (e: ChangeEvent<HTMLInputElement>) => { onChange: (e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
ndParams: { _custom: {
...filter.ndParams, ...filter._custom,
navidrome: {
...filter._custom?.navidrome,
has_rating: e.currentTarget.checked ? true : undefined, has_rating: e.currentTarget.checked ? true : undefined,
}, },
}, },
},
key: pageKey, key: pageKey,
}) as AlbumListFilter; }) as AlbumListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
}, },
value: filter.ndParams?.has_rating, value: filter._custom?.navidrome?.has_rating,
}, },
{ {
label: 'Is favorited', label: 'Is favorited',
onChange: (e: ChangeEvent<HTMLInputElement>) => { onChange: (e: ChangeEvent<HTMLInputElement>) => {
console.log('e.currentTarget.checked :>> ', e.currentTarget.checked);
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { 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, key: pageKey,
}) as AlbumListFilter; }) as AlbumListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
}, },
value: filter.ndParams?.starred, value: filter._custom?.navidrome?.starred,
}, },
{ {
label: 'Is compilation', label: 'Is compilation',
onChange: (e: ChangeEvent<HTMLInputElement>) => { onChange: (e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
ndParams: { _custom: {
...filter.ndParams, ...filter._custom,
navidrome: {
...filter._custom?.navidrome,
compilation: e.currentTarget.checked ? true : undefined, compilation: e.currentTarget.checked ? true : undefined,
}, },
}, },
},
key: pageKey, key: pageKey,
}) as AlbumListFilter; }) as AlbumListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
}, },
value: filter.ndParams?.compilation, value: filter._custom?.navidrome?.compilation,
}, },
{ {
label: 'Is recently played', label: 'Is recently played',
onChange: (e: ChangeEvent<HTMLInputElement>) => { onChange: (e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
ndParams: { _custom: {
...filter.ndParams, ...filter._custom,
navidrome: {
...filter._custom?.navidrome,
recently_played: e.currentTarget.checked ? true : undefined, recently_played: e.currentTarget.checked ? true : undefined,
}, },
}, },
},
key: pageKey, key: pageKey,
}) as AlbumListFilter; }) as AlbumListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
}, },
value: filter.ndParams?.recently_played, value: filter._custom?.navidrome?.recently_played,
}, },
]; ];
const handleYearFilter = debounce((e: number | string) => { const handleYearFilter = debounce((e: number | string) => {
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
ndParams: { _custom: {
...filter.ndParams, navidrome: {
...filter._custom?.navidrome,
year: e === '' ? undefined : (e as number), year: e === '' ? undefined : (e as number),
}, },
...filter._custom,
},
}, },
key: pageKey, key: pageKey,
}) as AlbumListFilter; }) as AlbumListFilter;
@ -125,18 +149,19 @@ export const NavidromeAlbumFilters = ({
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>(''); const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
const albumArtistListQuery = useAlbumArtistList( const albumArtistListQuery = useAlbumArtistList({
{ options: {
cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
query: {
// searchTerm: debouncedSearchTerm, // searchTerm: debouncedSearchTerm,
sortBy: AlbumArtistListSort.NAME, sortBy: AlbumArtistListSort.NAME,
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
startIndex: 0, startIndex: 0,
}, },
{ serverId,
cacheTime: 1000 * 60 * 2, });
staleTime: 1000 * 60 * 1,
},
);
const selectableAlbumArtists = useMemo(() => { const selectableAlbumArtists = useMemo(() => {
if (!albumArtistListQuery?.data?.items) return []; if (!albumArtistListQuery?.data?.items) return [];
@ -150,11 +175,14 @@ export const NavidromeAlbumFilters = ({
const handleAlbumArtistFilter = (e: string | null) => { const handleAlbumArtistFilter = (e: string | null) => {
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
ndParams: { _custom: {
...filter.ndParams, ...filter._custom,
navidrome: {
...filter._custom?.navidrome,
artist_id: e || undefined, artist_id: e || undefined,
}, },
}, },
},
key: pageKey, key: pageKey,
}) as AlbumListFilter; }) as AlbumListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
@ -177,7 +205,7 @@ export const NavidromeAlbumFilters = ({
<Divider my="0.5rem" /> <Divider my="0.5rem" />
<Group grow> <Group grow>
<NumberInput <NumberInput
defaultValue={filter.ndParams?.year} defaultValue={filter._custom?.navidrome?.year}
hideControls={false} hideControls={false}
label="Year" label="Year"
max={5000} max={5000}
@ -188,7 +216,7 @@ export const NavidromeAlbumFilters = ({
clearable clearable
searchable searchable
data={genreList} data={genreList}
defaultValue={filter.ndParams?.genre_id} defaultValue={filter._custom?.navidrome?.genre_id}
label="Genre" label="Genre"
onChange={handleGenresFilter} onChange={handleGenresFilter}
/> />
@ -198,7 +226,7 @@ export const NavidromeAlbumFilters = ({
clearable clearable
searchable searchable
data={selectableAlbumArtists} data={selectableAlbumArtists}
defaultValue={filter.ndParams?.artist_id} defaultValue={filter._custom?.navidrome?.artist_id}
disabled={disableArtistFilter} disabled={disableArtistFilter}
label="Artist" label="Artist"
limit={300} limit={300}

View file

@ -10,6 +10,7 @@ import { AlbumDetailHeader } from '/@/renderer/features/albums/components/album-
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { LibraryItem } from '/@/renderer/api/types'; import { LibraryItem } from '/@/renderer/api/types';
import { useCurrentServer } from '/@/renderer/store';
const AlbumDetailRoute = () => { const AlbumDetailRoute = () => {
const tableRef = useRef<AgGridReactType | null>(null); const tableRef = useRef<AgGridReactType | null>(null);
@ -17,7 +18,8 @@ const AlbumDetailRoute = () => {
const headerRef = useRef<HTMLDivElement>(null); const headerRef = useRef<HTMLDivElement>(null);
const { albumId } = useParams() as { albumId: string }; 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 background = useFastAverageColor(detailQuery.data?.imageUrl, !detailQuery.isLoading);
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();

View file

@ -1,4 +1,3 @@
import { VirtualInfiniteGridRef } from '/@/renderer/components';
import { AnimatedPage } from '/@/renderer/features/shared'; import { AnimatedPage } from '/@/renderer/features/shared';
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header'; import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content'; 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 { generatePageKey, useAlbumListFilter, useCurrentServer } from '/@/renderer/store';
import { useParams, useSearchParams } from 'react-router-dom'; import { useParams, useSearchParams } from 'react-router-dom';
import { AlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; import { AlbumListContext } from '/@/renderer/features/albums/context/album-list-context';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
const AlbumListRoute = () => { const AlbumListRoute = () => {
const gridRef = useRef<VirtualInfiniteGridRef | null>(null); const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
@ -24,17 +24,18 @@ const AlbumListRoute = () => {
const albumListFilter = useAlbumListFilter({ id: albumArtistId || undefined, key: pageKey }); const albumListFilter = useAlbumListFilter({ id: albumArtistId || undefined, key: pageKey });
const itemCountCheck = useAlbumList( const itemCountCheck = useAlbumList({
{ options: {
cacheTime: 1000 * 60,
staleTime: 1000 * 60,
},
query: {
limit: 1, limit: 1,
startIndex: 0, startIndex: 0,
...albumListFilter, ...albumListFilter,
}, },
{ serverId: server?.id,
cacheTime: 1000 * 60, });
staleTime: 1000 * 60,
},
);
const itemCount = const itemCount =
itemCountCheck.data?.totalRecordCount === null itemCountCheck.data?.totalRecordCount === null

View file

@ -1,12 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { import { Button, GridCarousel, Text, TextTitle } from '/@/renderer/components';
Button,
getColumnDefs,
GridCarousel,
Text,
TextTitle,
VirtualTable,
} from '/@/renderer/components';
import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core'; import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
import { Box, Group, Stack } from '@mantine/core'; import { Box, Group, Stack } from '@mantine/core';
import { RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri'; import { RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
@ -38,6 +31,7 @@ import {
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query'; import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query';
import { useTopSongsList } from '/@/renderer/features/artists/queries/top-songs-list-query'; import { useTopSongsList } from '/@/renderer/features/artists/queries/top-songs-list-query';
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
const ContentContainer = styled.div` const ContentContainer = styled.div`
position: relative; position: relative;
@ -63,7 +57,7 @@ export const AlbumArtistDetailContent = () => {
const server = useCurrentServer(); const server = useCurrentServer();
const itemsPerPage = cq.isXl ? 9 : cq.isLg ? 7 : cq.isMd ? 5 : cq.isSm ? 4 : 3; 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, { const artistDiscographyLink = `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY, {
albumArtistId, albumArtistId,
@ -80,34 +74,57 @@ export const AlbumArtistDetailContent = () => {
})}`; })}`;
const recentAlbumsQuery = useAlbumList({ const recentAlbumsQuery = useAlbumList({
jfParams: server?.type === ServerType.JELLYFIN ? { artistIds: albumArtistId } : undefined, query: {
limit: itemsPerPage, _custom: {
ndParams: jellyfin: {
server?.type === ServerType.NAVIDROME ...(server?.type === ServerType.JELLYFIN ? { artistIds: albumArtistId } : undefined),
},
navidrome: {
...(server?.type === ServerType.NAVIDROME
? { artist_id: albumArtistId, compilation: false } ? { artist_id: albumArtistId, compilation: false }
: undefined, : undefined),
},
},
limit: itemsPerPage,
sortBy: AlbumListSort.RELEASE_DATE, sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
startIndex: 0, startIndex: 0,
},
serverId: server?.id,
}); });
const compilationAlbumsQuery = useAlbumList({ const compilationAlbumsQuery = useAlbumList({
jfParams: query: {
server?.type === ServerType.JELLYFIN ? { contributingArtistIds: albumArtistId } : undefined, _custom: {
limit: itemsPerPage, jellyfin: {
ndParams: ...(server?.type === ServerType.JELLYFIN
server?.type === ServerType.NAVIDROME ? { contributingArtistIds: albumArtistId }
: undefined),
},
navidrome: {
...(server?.type === ServerType.NAVIDROME
? { artist_id: albumArtistId, compilation: true } ? { artist_id: albumArtistId, compilation: true }
: undefined, : undefined),
},
},
limit: itemsPerPage,
sortBy: AlbumListSort.RELEASE_DATE, sortBy: AlbumListSort.RELEASE_DATE,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
startIndex: 0, startIndex: 0,
},
serverId: server?.id,
}); });
const topSongsQuery = useTopSongsList( const topSongsQuery = useTopSongsList({
{ artist: detailQuery?.data?.name || '', artistId: albumArtistId }, options: {
{ enabled: !!detailQuery?.data?.name }, enabled: !!detailQuery?.data?.name,
); },
query: {
artist: detailQuery?.data?.name || '',
artistId: albumArtistId,
},
serverId: server?.id,
});
const topSongsColumnDefs: ColDef[] = useMemo( const topSongsColumnDefs: ColDef[] = useMemo(
() => () =>
@ -242,8 +259,8 @@ export const AlbumArtistDetailContent = () => {
}); });
}; };
const createFavoriteMutation = useCreateFavorite(); const createFavoriteMutation = useCreateFavorite({});
const deleteFavoriteMutation = useDeleteFavorite(); const deleteFavoriteMutation = useDeleteFavorite({});
const handleFavorite = () => { const handleFavorite = () => {
if (!detailQuery?.data) return; if (!detailQuery?.data) return;

View file

@ -1,13 +1,14 @@
import { Group, Rating, Stack } from '@mantine/core';
import { forwardRef, Fragment, Ref, MouseEvent } from 'react'; import { forwardRef, Fragment, Ref, MouseEvent } from 'react';
import { Group, Rating, Stack } from '@mantine/core';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { LibraryItem, ServerType } from '/@/renderer/api/types'; import { LibraryItem, ServerType } from '/@/renderer/api/types';
import { Text } from '/@/renderer/components'; import { Text } from '/@/renderer/components';
import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query'; 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 { useContainerQuery } from '/@/renderer/hooks';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { formatDurationString } from '/@/renderer/utils'; import { formatDurationString } from '/@/renderer/utils';
import { useCurrentServer } from '../../../store/auth.store';
interface AlbumArtistDetailHeaderProps { interface AlbumArtistDetailHeaderProps {
background: string; background: string;
@ -16,7 +17,11 @@ interface AlbumArtistDetailHeaderProps {
export const AlbumArtistDetailHeader = forwardRef( export const AlbumArtistDetailHeader = forwardRef(
({ background }: AlbumArtistDetailHeaderProps, ref: Ref<HTMLDivElement>) => { ({ background }: AlbumArtistDetailHeaderProps, ref: Ref<HTMLDivElement>) => {
const { albumArtistId } = useParams() as { albumArtistId: string }; 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 cq = useContainerQuery();
const metadataItems = [ const metadataItems = [
@ -37,17 +42,17 @@ export const AlbumArtistDetailHeader = forwardRef(
}, },
]; ];
const updateRatingMutation = useUpdateRating(); const updateRatingMutation = useSetRating({});
const handleUpdateRating = (rating: number) => { const handleUpdateRating = (rating: number) => {
if (!detailQuery?.data) return; if (!detailQuery?.data) return;
updateRatingMutation.mutate({ updateRatingMutation.mutate({
_serverId: detailQuery?.data.serverId,
query: { query: {
item: [detailQuery.data], item: [detailQuery.data],
rating, rating,
}, },
serverId: detailQuery?.data.serverId,
}); });
}; };
@ -58,11 +63,11 @@ export const AlbumArtistDetailHeader = forwardRef(
if (!isSameRatingAsPrevious) return; if (!isSameRatingAsPrevious) return;
updateRatingMutation.mutate({ updateRatingMutation.mutate({
_serverId: detailQuery.data.serverId,
query: { query: {
item: [detailQuery.data], item: [detailQuery.data],
rating: 0, rating: 0,
}, },
serverId: detailQuery.data.serverId,
}); });
}; };

View file

@ -1,13 +1,14 @@
import { MutableRefObject, useMemo } from 'react'; import { MutableRefObject, useMemo } from 'react';
import type { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core'; import type { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; 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 { useCurrentServer, useSongListStore } from '/@/renderer/store';
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu'; import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { LibraryItem, QueueSong } from '/@/renderer/api/types'; import { LibraryItem, QueueSong } from '/@/renderer/api/types';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
interface AlbumArtistSongListContentProps { interface AlbumArtistSongListContentProps {
data: QueueSong[]; data: QueueSong[];

View file

@ -1,12 +1,4 @@
import { import { ALBUMARTIST_CARD_ROWS } from '/@/renderer/components';
ALBUMARTIST_CARD_ROWS,
getColumnDefs,
TablePagination,
VirtualGridAutoSizerContainer,
VirtualInfiniteGrid,
VirtualInfiniteGridRef,
VirtualTable,
} from '/@/renderer/components';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { ListDisplayType, CardRow } from '/@/renderer/types'; import { ListDisplayType, CardRow } from '/@/renderer/types';
import AutoSizer from 'react-virtualized-auto-sizer'; 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 { usePlayQueueAdd } from '/@/renderer/features/player';
import { useAlbumArtistListFilter, useListStoreActions } from '../../../store/list.store'; import { useAlbumArtistListFilter, useListStoreActions } from '../../../store/list.store';
import { useAlbumArtistListContext } from '/@/renderer/features/artists/context/album-artist-list-context'; 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 { interface AlbumArtistListContentProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>; gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
@ -54,17 +52,18 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon
const isPaginationEnabled = display === ListDisplayType.TABLE_PAGINATED; const isPaginationEnabled = display === ListDisplayType.TABLE_PAGINATED;
const checkAlbumArtistList = useAlbumArtistList( const checkAlbumArtistList = useAlbumArtistList({
{ options: {
cacheTime: Infinity,
staleTime: 60 * 1000 * 5,
},
query: {
limit: 1, limit: 1,
startIndex: 0, startIndex: 0,
...filter, ...filter,
}, },
{ serverId: server?.id,
cacheTime: Infinity, });
staleTime: 60 * 1000 * 5,
},
);
const columnDefs: ColDef[] = useMemo(() => getColumnDefs(table.columns), [table.columns]); const columnDefs: ColDef[] = useMemo(() => getColumnDefs(table.columns), [table.columns]);
@ -85,19 +84,23 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getAlbumArtistList({ api.controller.getAlbumArtistList({
apiClientProps: {
server,
signal,
},
query: { query: {
limit, limit,
startIndex, startIndex,
...filter, ...filter,
}, },
server,
signal,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );
const albums = api.normalize.albumArtistList(albumArtistsRes, server); params.successCallback(
params.successCallback(albums?.items || [], albumArtistsRes?.totalRecordCount || 0); albumArtistsRes?.items || [],
albumArtistsRes?.totalRecordCount || 0,
);
}, },
rowCount: undefined, rowCount: undefined,
}; };
@ -181,18 +184,20 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getAlbumArtistList({ api.controller.getAlbumArtistList({
apiClientProps: {
server,
signal,
},
query: { query: {
limit, limit,
startIndex, startIndex,
...filter, ...filter,
}, },
server,
signal,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );
return api.normalize.albumArtistList(albumArtistsRes, server); return albumArtistsRes;
}, },
[filter, queryClient, server], [filter, queryClient, server],
); );
@ -259,6 +264,7 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon
{display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? ( {display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? (
<AutoSizer> <AutoSizer>
{({ height, width }) => ( {({ height, width }) => (
<>
<VirtualInfiniteGrid <VirtualInfiniteGrid
ref={gridRef} ref={gridRef}
cardRows={cardRows} cardRows={cardRows}
@ -280,6 +286,7 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon
width={width} width={width}
onScroll={handleGridScroll} onScroll={handleGridScroll}
/> />
</>
)} )}
</AutoSizer> </AutoSizer>
) : ( ) : (

View file

@ -15,16 +15,7 @@ import {
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { AlbumArtistListSort, SortOrder } from '/@/renderer/api/types'; import { AlbumArtistListSort, SortOrder } from '/@/renderer/api/types';
import { import { DropdownMenu, Text, Button, Slider, MultiSelect, Switch } from '/@/renderer/components';
DropdownMenu,
ALBUMARTIST_TABLE_COLUMNS,
VirtualInfiniteGridRef,
Text,
Button,
Slider,
MultiSelect,
Switch,
} from '/@/renderer/components';
import { useMusicFolders } from '/@/renderer/features/shared'; import { useMusicFolders } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { import {
@ -36,6 +27,8 @@ import {
} from '/@/renderer/store'; } from '/@/renderer/store';
import { ListDisplayType, TableColumn, ServerType } from '/@/renderer/types'; import { ListDisplayType, TableColumn, ServerType } from '/@/renderer/types';
import { useAlbumArtistListContext } from '../context/album-artist-list-context'; 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 = { const FILTERS = {
jellyfin: [ jellyfin: [
@ -83,7 +76,7 @@ export const AlbumArtistListHeaderFilters = ({
const filter = useAlbumArtistListFilter({ key: pageKey }); const filter = useAlbumArtistListFilter({ key: pageKey });
const cq = useContainerQuery(); const cq = useContainerQuery();
const musicFoldersQuery = useMusicFolders(); const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id });
const sortByLabel = const sortByLabel =
(server?.type && (server?.type &&
@ -114,18 +107,20 @@ export const AlbumArtistListHeaderFilters = ({
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getAlbumArtistList({ api.controller.getAlbumArtistList({
apiClientProps: {
server,
signal,
},
query: { query: {
limit, limit,
startIndex, startIndex,
...filters, ...filters,
}, },
server,
signal,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );
return api.normalize.albumArtistList(albums, server); return albums;
}, },
[queryClient, server], [queryClient, server],
); );
@ -148,20 +143,21 @@ export const AlbumArtistListHeaderFilters = ({
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getAlbumArtistList({ api.controller.getAlbumArtistList({
apiClientProps: {
server,
signal,
},
query: { query: {
limit, limit,
startIndex, startIndex,
...filters, ...filters,
}, },
server,
signal,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );
const albumArtists = api.normalize.albumArtistList(albumArtistsRes, server);
params.successCallback( params.successCallback(
albumArtists?.items || [], albumArtistsRes?.items || [],
albumArtistsRes?.totalRecordCount || 0, albumArtistsRes?.totalRecordCount || 0,
); );
}, },
@ -355,7 +351,7 @@ export const AlbumArtistListHeaderFilters = ({
</Button> </Button>
</DropdownMenu.Target> </DropdownMenu.Target>
<DropdownMenu.Dropdown> <DropdownMenu.Dropdown>
{musicFoldersQuery.data?.map((folder) => ( {musicFoldersQuery.data?.items.map((folder) => (
<DropdownMenu.Item <DropdownMenu.Item
key={`musicFolder-${folder.id}`} key={`musicFolder-${folder.id}`}
$isActive={filter.musicFolderId === folder.id} $isActive={filter.musicFolderId === folder.id}

View file

@ -7,7 +7,7 @@ import { useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { PageHeader, SearchInput, VirtualInfiniteGridRef } from '/@/renderer/components'; import { PageHeader, SearchInput } from '/@/renderer/components';
import { LibraryHeaderBar } from '/@/renderer/features/shared'; import { LibraryHeaderBar } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { import {
@ -20,6 +20,7 @@ import { ListDisplayType } from '/@/renderer/types';
import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters'; import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters';
import { useAlbumArtistListContext } from '/@/renderer/features/artists/context/album-artist-list-context'; import { useAlbumArtistListContext } from '/@/renderer/features/artists/context/album-artist-list-context';
import { FilterBar } from '../../shared/components/filter-bar'; import { FilterBar } from '../../shared/components/filter-bar';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
interface AlbumArtistListHeaderProps { interface AlbumArtistListHeaderProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>; gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
@ -51,18 +52,20 @@ export const AlbumArtistListHeader = ({
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getAlbumArtistList({ api.controller.getAlbumArtistList({
apiClientProps: {
server,
signal,
},
query: { query: {
limit, limit,
startIndex, startIndex,
...filters, ...filters,
}, },
server,
signal,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );
return api.normalize.albumArtistList(albums, server); return albums;
}, },
[queryClient, server], [queryClient, server],
); );
@ -85,20 +88,21 @@ export const AlbumArtistListHeader = ({
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getAlbumArtistList({ api.controller.getAlbumArtistList({
apiClientProps: {
server,
signal,
},
query: { query: {
limit, limit,
startIndex, startIndex,
...filters, ...filters,
}, },
server,
signal,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );
const albumArtists = api.normalize.albumArtistList(albumArtistsRes, server);
params.successCallback( params.successCallback(
albumArtists?.items || [], albumArtistsRes?.items || [],
albumArtistsRes?.totalRecordCount || 0, albumArtistsRes?.totalRecordCount || 0,
); );
}, },

View file

@ -9,15 +9,17 @@ import { LibraryItem } from '/@/renderer/api/types';
import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query'; import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query';
import { AlbumArtistDetailHeader } from '/@/renderer/features/artists/components/album-artist-detail-header'; import { AlbumArtistDetailHeader } from '/@/renderer/features/artists/components/album-artist-detail-header';
import { AlbumArtistDetailContent } from '/@/renderer/features/artists/components/album-artist-detail-content'; import { AlbumArtistDetailContent } from '/@/renderer/features/artists/components/album-artist-detail-content';
import { useCurrentServer } from '/@/renderer/store';
const AlbumArtistDetailRoute = () => { const AlbumArtistDetailRoute = () => {
const scrollAreaRef = useRef<HTMLDivElement>(null); const scrollAreaRef = useRef<HTMLDivElement>(null);
const headerRef = useRef<HTMLDivElement>(null); const headerRef = useRef<HTMLDivElement>(null);
const server = useCurrentServer();
const { albumArtistId } = useParams() as { albumArtistId: string }; const { albumArtistId } = useParams() as { albumArtistId: string };
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
const playButtonBehavior = usePlayButtonBehavior(); 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 background = useFastAverageColor(detailQuery.data?.imageUrl, !detailQuery.isLoading);
const handlePlay = () => { const handlePlay = () => {

View file

@ -1,22 +1,25 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useRef } from 'react'; import { useRef } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { AlbumArtistDetailTopSongsListContent } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-content'; 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 { 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 { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query';
import { useTopSongsList } from '/@/renderer/features/artists/queries/top-songs-list-query'; import { useTopSongsList } from '/@/renderer/features/artists/queries/top-songs-list-query';
import { AnimatedPage } from '/@/renderer/features/shared'; import { AnimatedPage } from '/@/renderer/features/shared';
import { useCurrentServer } from '../../../store/auth.store';
const AlbumArtistDetailTopSongsListRoute = () => { const AlbumArtistDetailTopSongsListRoute = () => {
const tableRef = useRef<AgGridReactType | null>(null); const tableRef = useRef<AgGridReactType | null>(null);
const { albumArtistId } = useParams() as { albumArtistId: string }; 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( const topSongsQuery = useTopSongsList({
{ artist: detailQuery?.data?.name || '', artistId: albumArtistId }, options: { enabled: !!detailQuery?.data?.name },
{ enabled: !!detailQuery?.data?.name }, query: { artist: detailQuery?.data?.name || '', artistId: albumArtistId },
); serverId: server?.id,
});
const itemCount = topSongsQuery?.data?.items?.length || 0; const itemCount = topSongsQuery?.data?.items?.length || 0;

View file

@ -1,4 +1,3 @@
import { VirtualInfiniteGridRef } from '/@/renderer/components';
import { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header'; import { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header';
import { AnimatedPage } from '/@/renderer/features/shared'; import { AnimatedPage } from '/@/renderer/features/shared';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; 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 { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
import { generatePageKey, useAlbumArtistListFilter } from '/@/renderer/store'; import { generatePageKey, useAlbumArtistListFilter } from '/@/renderer/store';
import { AlbumArtistListContext } from '/@/renderer/features/artists/context/album-artist-list-context'; 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 AlbumArtistListRoute = () => {
const gridRef = useRef<VirtualInfiniteGridRef | null>(null); const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
const tableRef = useRef<AgGridReactType | null>(null); const tableRef = useRef<AgGridReactType | null>(null);
const pageKey = generatePageKey('albumArtist', undefined); const pageKey = generatePageKey('albumArtist', undefined);
const server = useCurrentServer();
const albumArtistListFilter = useAlbumArtistListFilter({ id: undefined, key: pageKey }); const albumArtistListFilter = useAlbumArtistListFilter({ id: undefined, key: pageKey });
const itemCountCheck = useAlbumArtistList( const itemCountCheck = useAlbumArtistList({
{ options: {
cacheTime: 1000 * 60,
staleTime: 1000 * 60,
},
query: {
limit: 1, limit: 1,
startIndex: 0, startIndex: 0,
...albumArtistListFilter, ...albumArtistListFilter,
}, },
{ serverId: server?.id,
cacheTime: 1000 * 60, });
staleTime: 1000 * 60,
},
);
const itemCount = const itemCount =
itemCountCheck.data?.totalRecordCount === null itemCountCheck.data?.totalRecordCount === null

View file

@ -43,7 +43,7 @@ import {
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { useDeletePlaylist } from '/@/renderer/features/playlists'; import { useDeletePlaylist } from '/@/renderer/features/playlists';
import { useRemoveFromPlaylist } from '/@/renderer/features/playlists/mutations/remove-from-playlist-mutation'; 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 { useAuthStore, useCurrentServer, useQueueControls } from '/@/renderer/store';
import { usePlayerType } from '/@/renderer/store/settings.store'; import { usePlayerType } from '/@/renderer/store/settings.store';
import { Play, PlaybackType } from '/@/renderer/types'; import { Play, PlaybackType } from '/@/renderer/types';
@ -190,7 +190,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
[ctx.data, ctx.type, handlePlayQueueAdd], [ctx.data, ctx.type, handlePlayQueueAdd],
); );
const deletePlaylistMutation = useDeletePlaylist(); const deletePlaylistMutation = useDeletePlaylist({});
const handleDeletePlaylist = useCallback(() => { const handleDeletePlaylist = useCallback(() => {
for (const item of ctx.data) { for (const item of ctx.data) {
@ -236,8 +236,8 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
}); });
}, [ctx.data, handleDeletePlaylist]); }, [ctx.data, handleDeletePlaylist]);
const createFavoriteMutation = useCreateFavorite(); const createFavoriteMutation = useCreateFavorite({});
const deleteFavoriteMutation = useDeleteFavorite(); const deleteFavoriteMutation = useDeleteFavorite({});
const handleAddToFavorites = useCallback(() => { const handleAddToFavorites = useCallback(() => {
if (!ctx.dataNodes && !ctx.data) return; if (!ctx.dataNodes && !ctx.data) return;
@ -414,7 +414,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
serverType, serverType,
]); ]);
const updateRatingMutation = useUpdateRating(); const updateRatingMutation = useSetRating({});
const handleUpdateRating = useCallback( const handleUpdateRating = useCallback(
(rating: number) => { (rating: number) => {
@ -450,11 +450,11 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
updateRatingMutation.mutate( updateRatingMutation.mutate(
{ {
_serverId: serverId,
query: { query: {
item: items, item: items,
rating, rating,
}, },
serverId,
}, },
{ {
onSuccess: () => { onSuccess: () => {

View file

@ -23,75 +23,80 @@ const HomeRoute = () => {
recentlyPlayed: 0, recentlyPlayed: 0,
}); });
const feature = useAlbumList( const feature = useAlbumList({
{ options: {
cacheTime: 1000 * 60,
staleTime: 1000 * 60,
},
query: {
limit: 20, limit: 20,
sortBy: AlbumListSort.RANDOM, sortBy: AlbumListSort.RANDOM,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
startIndex: 0, startIndex: 0,
}, },
{ serverId: server?.id,
cacheTime: 1000 * 60, });
staleTime: 1000 * 60,
},
);
const featureItemsWithImage = useMemo(() => { const featureItemsWithImage = useMemo(() => {
return feature.data?.items?.filter((item) => item.imageUrl) ?? []; return feature.data?.items?.filter((item) => item.imageUrl) ?? [];
}, [feature.data?.items]); }, [feature.data?.items]);
const random = useAlbumList( const random = useAlbumList({
{ options: {
cacheTime: 1000 * 60,
keepPreviousData: true,
staleTime: 1000 * 60,
},
query: {
limit: itemsPerPage, limit: itemsPerPage,
sortBy: AlbumListSort.RANDOM, sortBy: AlbumListSort.RANDOM,
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
startIndex: pagination.random * itemsPerPage, startIndex: pagination.random * itemsPerPage,
}, },
{ serverId: server?.id,
cacheTime: 1000 * 60, });
keepPreviousData: true,
staleTime: 1000 * 60,
},
);
const recentlyPlayed = useRecentlyPlayed( const recentlyPlayed = useRecentlyPlayed({
{ options: {
keepPreviousData: true,
staleTime: 0,
},
query: {
limit: itemsPerPage, limit: itemsPerPage,
sortBy: AlbumListSort.RECENTLY_PLAYED, sortBy: AlbumListSort.RECENTLY_PLAYED,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
startIndex: pagination.recentlyPlayed * itemsPerPage, startIndex: pagination.recentlyPlayed * itemsPerPage,
}, },
{ serverId: server?.id,
keepPreviousData: true, });
staleTime: 0,
},
);
const recentlyAdded = useAlbumList( const recentlyAdded = useAlbumList({
{ options: {
keepPreviousData: true,
staleTime: 1000 * 60,
},
query: {
limit: itemsPerPage, limit: itemsPerPage,
sortBy: AlbumListSort.RECENTLY_ADDED, sortBy: AlbumListSort.RECENTLY_ADDED,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
startIndex: pagination.recentlyAdded * itemsPerPage, startIndex: pagination.recentlyAdded * itemsPerPage,
}, },
{ serverId: server?.id,
keepPreviousData: true, });
staleTime: 1000 * 60,
},
);
const mostPlayed = useAlbumList( const mostPlayed = useAlbumList({
{ options: {
keepPreviousData: true,
staleTime: 1000 * 60 * 60,
},
query: {
limit: itemsPerPage, limit: itemsPerPage,
sortBy: AlbumListSort.PLAY_COUNT, sortBy: AlbumListSort.PLAY_COUNT,
sortOrder: SortOrder.DESC, sortOrder: SortOrder.DESC,
startIndex: pagination.mostPlayed * itemsPerPage, startIndex: pagination.mostPlayed * itemsPerPage,
}, },
{ serverId: server?.id,
keepPreviousData: true, });
staleTime: 1000 * 60 * 60,
},
);
const handleNextPage = useCallback( const handleNextPage = useCallback(
(key: 'mostPlayed' | 'random' | 'recentlyAdded' | 'recentlyPlayed') => { (key: 'mostPlayed' | 'random' | 'recentlyAdded' | 'recentlyPlayed') => {

View file

@ -1,7 +1,7 @@
import type { MutableRefObject } from 'react'; import type { MutableRefObject } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Group } from '@mantine/core'; import { Group } from '@mantine/core';
import { Button, Popover, TableConfigDropdown } from '/@/renderer/components'; import { Button, Popover } from '/@/renderer/components';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { import {
RiArrowDownLine, RiArrowDownLine,
@ -16,6 +16,7 @@ import { usePlayerControls, useQueueControls } from '/@/renderer/store';
import { PlaybackType, TableType } from '/@/renderer/types'; import { PlaybackType, TableType } from '/@/renderer/types';
import { usePlayerType } from '/@/renderer/store/settings.store'; import { usePlayerType } from '/@/renderer/store/settings.store';
import { useSetCurrentTime } from '../../../store/player.store'; import { useSetCurrentTime } from '../../../store/player.store';
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;

View file

@ -8,7 +8,6 @@ import type {
} from '@ag-grid-community/core'; } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import '@ag-grid-community/styles/ag-theme-alpine.css'; import '@ag-grid-community/styles/ag-theme-alpine.css';
import { VirtualGridAutoSizerContainer, getColumnDefs } from '/@/renderer/components';
import { import {
useAppStoreActions, useAppStoreActions,
useCurrentSong, useCurrentSong,
@ -27,12 +26,13 @@ import { useMergedRef } from '@mantine/hooks';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { ErrorBoundary } from 'react-error-boundary'; 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 { ErrorFallback } from '/@/renderer/features/action-required';
import { PlaybackType, TableType } from '/@/renderer/types'; import { PlaybackType, TableType } from '/@/renderer/types';
import { LibraryItem, QueueSong } from '/@/renderer/api/types'; import { LibraryItem, QueueSong } from '/@/renderer/api/types';
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu'; import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; 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 mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
const utils = isElectron() ? window.electron.utils : null; const utils = isElectron() ? window.electron.utils : null;

View file

@ -4,8 +4,9 @@ import { NowPlayingHeader } from '/@/renderer/features/now-playing/components/no
import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue'; import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue';
import type { Song } from '/@/renderer/api/types'; import type { Song } from '/@/renderer/api/types';
import { AnimatedPage } from '/@/renderer/features/shared'; 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 { PlayQueueListControls } from '/@/renderer/features/now-playing/components/play-queue-list-controls';
import { VirtualGridContainer } from '/@/renderer/components/virtual-grid';
const NowPlayingRoute = () => { const NowPlayingRoute = () => {
const queueRef = useRef<{ grid: AgGridReactType<Song> } | null>(null); const queueRef = useRef<{ grid: AgGridReactType<Song> } | null>(null);

View file

@ -5,7 +5,7 @@ import { Variants, motion } from 'framer-motion';
import { RiArrowDownSLine, RiSettings3Line } from 'react-icons/ri'; import { RiArrowDownSLine, RiSettings3Line } from 'react-icons/ri';
import { useLocation } from 'react-router'; import { useLocation } from 'react-router';
import styled from 'styled-components'; import styled from 'styled-components';
import { Button, Option, Popover, Switch, TableConfigDropdown } from '/@/renderer/components'; import { Button, Option, Popover, Switch } from '/@/renderer/components';
import { import {
useCurrentSong, useCurrentSong,
useFullScreenPlayerStore, useFullScreenPlayerStore,
@ -14,6 +14,7 @@ import {
import { useFastAverageColor } from '../../../hooks/use-fast-average-color'; import { useFastAverageColor } from '../../../hooks/use-fast-average-color';
import { FullScreenPlayerImage } from '/@/renderer/features/player/components/full-screen-player-image'; import { FullScreenPlayerImage } from '/@/renderer/features/player/components/full-screen-player-image';
import { FullScreenPlayerQueue } from '/@/renderer/features/player/components/full-screen-player-queue'; import { FullScreenPlayerQueue } from '/@/renderer/features/player/components/full-screen-player-queue';
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
const Container = styled(motion.div)` const Container = styled(motion.div)`
z-index: 100; z-index: 100;

View file

@ -19,7 +19,7 @@ import {
import { useRightControls } from '../hooks/use-right-controls'; import { useRightControls } from '../hooks/use-right-controls';
import { PlayerButton } from './player-button'; import { PlayerButton } from './player-button';
import { LibraryItem, ServerType } from '/@/renderer/api/types'; 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 { Rating } from '/@/renderer/components';
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider'; import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
@ -32,9 +32,9 @@ export const RightControls = () => {
const { rightExpanded: isQueueExpanded } = useSidebarStore(); const { rightExpanded: isQueueExpanded } = useSidebarStore();
const { handleVolumeSlider, handleVolumeWheel, handleMute } = useRightControls(); const { handleVolumeSlider, handleVolumeWheel, handleMute } = useRightControls();
const updateRatingMutation = useUpdateRating(); const updateRatingMutation = useSetRating({});
const addToFavoritesMutation = useCreateFavorite(); const addToFavoritesMutation = useCreateFavorite({});
const removeFromFavoritesMutation = useDeleteFavorite(); const removeFromFavoritesMutation = useDeleteFavorite({});
const handleAddToFavorites = () => { const handleAddToFavorites = () => {
if (!currentSong) return; if (!currentSong) return;
@ -51,11 +51,11 @@ export const RightControls = () => {
if (!currentSong) return; if (!currentSong) return;
updateRatingMutation.mutate({ updateRatingMutation.mutate({
_serverId: currentSong?.serverId,
query: { query: {
item: [currentSong], item: [currentSong],
rating, rating,
}, },
serverId: currentSong?.serverId,
}); });
}; };
@ -63,11 +63,11 @@ export const RightControls = () => {
if (!currentSong || !rating) return; if (!currentSong || !rating) return;
updateRatingMutation.mutate({ updateRatingMutation.mutate({
_serverId: currentSong?.serverId,
query: { query: {
item: [currentSong], item: [currentSong],
rating: 0, rating: 0,
}, },
serverId: currentSong?.serverId,
}); });
}; };

View file

@ -1,20 +1,11 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { api } from '/@/renderer/api/index'; 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 { queryKeys } from '/@/renderer/api/query-keys';
import { import { useCurrentServer, usePlayerControls, usePlayerStore } from '/@/renderer/store';
useAuthStore,
useCurrentServer,
usePlayerControls,
usePlayerStore,
} from '/@/renderer/store';
import { usePlayerType } from '/@/renderer/store/settings.store'; import { usePlayerType } from '/@/renderer/store/settings.store';
import { PlayQueueAddOptions, Play, PlaybackType } from '/@/renderer/types'; 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 isElectron from 'is-electron';
import { nanoid } from 'nanoid/non-secure'; import { nanoid } from 'nanoid/non-secure';
import { LibraryItem, SongListSort, SortOrder } from '/@/renderer/api/types'; import { LibraryItem, SongListSort, SortOrder } from '/@/renderer/api/types';
@ -25,7 +16,6 @@ const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
export const useHandlePlayQueueAdd = () => { export const useHandlePlayQueueAdd = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const playerType = usePlayerType(); const playerType = usePlayerType();
const deviceId = useAuthStore.getState().deviceId;
const server = useCurrentServer(); const server = useCurrentServer();
const { play } = usePlayerControls(); const { play } = usePlayerControls();
@ -114,9 +104,11 @@ export const useHandlePlayQueueAdd = () => {
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getPlaylistSongList({ api.controller.getPlaylistSongList({
query: queryFilter, apiClientProps: {
server, server,
signal, signal,
},
query: queryFilter,
}), }),
{ {
cacheTime: 1000 * 60, cacheTime: 1000 * 60,
@ -128,9 +120,11 @@ export const useHandlePlayQueueAdd = () => {
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getSongList({ api.controller.getSongList({
query: queryFilter, apiClientProps: {
server, server,
signal, signal,
},
query: queryFilter,
}), }),
{ {
cacheTime: 1000 * 60, cacheTime: 1000 * 60,
@ -147,20 +141,7 @@ export const useHandlePlayQueueAdd = () => {
if (!songsList) return toast.warn({ message: 'No songs found' }); if (!songsList) return toast.warn({ message: 'No songs found' });
switch (server?.type) { songs = songsList.items?.map((song) => ({ ...song, uniqueId: nanoid() }));
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;
}
} else if (options.byData) { } else if (options.byData) {
songs = options.byData.map((song) => ({ ...song, uniqueId: nanoid() })); songs = options.byData.map((song) => ({ ...song, uniqueId: nanoid() }));
} }
@ -207,7 +188,7 @@ export const useHandlePlayQueueAdd = () => {
return null; return null;
}, },
[deviceId, play, playerType, queryClient, server], [play, playerType, queryClient, server],
); );
return handlePlayQueueAdd; return handlePlayQueueAdd;

View file

@ -67,13 +67,13 @@ export const useScrobble = () => {
currentSong?.serverType === ServerType.JELLYFIN ? currentTime * 1e7 : undefined; currentSong?.serverType === ServerType.JELLYFIN ? currentTime * 1e7 : undefined;
sendScrobble.mutate({ sendScrobble.mutate({
_serverId: currentSong?.serverId,
query: { query: {
event: 'timeupdate', event: 'timeupdate',
id: currentSong.id, id: currentSong.id,
position, position,
submission: false, submission: false,
}, },
serverId: currentSong?.serverId,
}); });
}, },
[isScrobbleEnabled, sendScrobble], [isScrobbleEnabled, sendScrobble],
@ -110,12 +110,12 @@ export const useScrobble = () => {
previousSong?.serverType === ServerType.JELLYFIN ? previousSongTime * 1e7 : undefined; previousSong?.serverType === ServerType.JELLYFIN ? previousSongTime * 1e7 : undefined;
sendScrobble.mutate({ sendScrobble.mutate({
_serverId: previousSong?.serverId,
query: { query: {
id: previousSong.id, id: previousSong.id,
position, position,
submission: true, submission: true,
}, },
serverId: previousSong?.serverId,
}); });
} }
} }
@ -130,13 +130,13 @@ export const useScrobble = () => {
// Send start scrobble when song changes and the new song is playing // Send start scrobble when song changes and the new song is playing
if (status === PlayerStatus.PLAYING && currentSong?.id) { if (status === PlayerStatus.PLAYING && currentSong?.id) {
sendScrobble.mutate({ sendScrobble.mutate({
_serverId: currentSong?.serverId,
query: { query: {
event: 'start', event: 'start',
id: currentSong.id, id: currentSong.id,
position: 0, position: 0,
submission: false, submission: false,
}, },
serverId: currentSong?.serverId,
}); });
if (currentSong?.serverType === ServerType.JELLYFIN) { if (currentSong?.serverType === ServerType.JELLYFIN) {
@ -175,13 +175,13 @@ export const useScrobble = () => {
// Whenever the player is restarted, send a 'start' scrobble // Whenever the player is restarted, send a 'start' scrobble
if (status === PlayerStatus.PLAYING) { if (status === PlayerStatus.PLAYING) {
sendScrobble.mutate({ sendScrobble.mutate({
_serverId: currentSong?.serverId,
query: { query: {
event: 'unpause', event: 'unpause',
id: currentSong.id, id: currentSong.id,
position, position,
submission: false, submission: false,
}, },
serverId: currentSong?.serverId,
}); });
if (currentSong?.serverType === ServerType.JELLYFIN) { 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 // Jellyfin is the only one that needs to send a 'pause' event to the server
} else if (currentSong?.serverType === ServerType.JELLYFIN) { } else if (currentSong?.serverType === ServerType.JELLYFIN) {
sendScrobble.mutate({ sendScrobble.mutate({
_serverId: currentSong?.serverId,
query: { query: {
event: 'pause', event: 'pause',
id: currentSong.id, id: currentSong.id,
position, position,
submission: false, submission: false,
}, },
serverId: currentSong?.serverId,
}); });
if (progressIntervalId.current) { if (progressIntervalId.current) {
@ -217,11 +217,11 @@ export const useScrobble = () => {
if (!isCurrentSongScrobbled && shouldSubmitScrobble) { if (!isCurrentSongScrobbled && shouldSubmitScrobble) {
sendScrobble.mutate({ sendScrobble.mutate({
_serverId: currentSong?.serverId,
query: { query: {
id: currentSong.id, id: currentSong.id,
submission: true, submission: true,
}, },
serverId: currentSong?.serverId,
}); });
setIsCurrentSongScrobbled(true); setIsCurrentSongScrobbled(true);
@ -261,24 +261,24 @@ export const useScrobble = () => {
if (!isCurrentSongScrobbled && shouldSubmitScrobble) { if (!isCurrentSongScrobbled && shouldSubmitScrobble) {
sendScrobble.mutate({ sendScrobble.mutate({
_serverId: currentSong?.serverId,
query: { query: {
id: currentSong.id, id: currentSong.id,
position, position,
submission: true, submission: true,
}, },
serverId: currentSong?.serverId,
}); });
} }
if (currentSong?.serverType === ServerType.JELLYFIN) { if (currentSong?.serverType === ServerType.JELLYFIN) {
sendScrobble.mutate({ sendScrobble.mutate({
_serverId: currentSong?.serverId,
query: { query: {
event: 'start', event: 'start',
id: currentSong.id, id: currentSong.id,
position: 0, position: 0,
submission: false, submission: false,
}, },
serverId: currentSong?.serverId,
}); });
} }

View file

@ -23,15 +23,20 @@ export const AddToPlaylistContextModal = ({
const server = useCurrentServer(); const server = useCurrentServer();
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const addToPlaylistMutation = useAddToPlaylist(); const addToPlaylistMutation = useAddToPlaylist({});
const playlistList = usePlaylistList({ const playlistList = usePlaylistList({
ndParams: { query: {
_custom: {
navidrome: {
smart: false, smart: false,
}, },
},
sortBy: PlaylistListSort.NAME, sortBy: PlaylistListSort.NAME,
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
startIndex: 0, startIndex: 0,
},
serverId: server?.id,
}); });
const playlistSelect = useMemo(() => { const playlistSelect = useMemo(() => {
@ -60,11 +65,12 @@ export const AddToPlaylistContextModal = ({
const queryKey = queryKeys.songs.list(server?.id || '', query); const queryKey = queryKeys.songs.list(server?.id || '', query);
const songsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => const songsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => {
api.controller.getSongList({ query, server, 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) => { const getSongsByArtist = async (artistId: string) => {
@ -77,11 +83,12 @@ export const AddToPlaylistContextModal = ({
const queryKey = queryKeys.songs.list(server?.id || '', query); const queryKey = queryKeys.songs.list(server?.id || '', query);
const songsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => const songsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => {
api.controller.getSongList({ query, server, 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; 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 queryKey = queryKeys.playlists.songList(server?.id || '', playlistId, query);
const playlistSongsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => const playlistSongsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => {
api.controller.getPlaylistSongList({ if (!server) throw new Error('No server');
query: { id: playlistId, startIndex: 0 }, return api.controller.getPlaylistSongList({
apiClientProps: {
server, server,
signal, signal,
}), },
); query: { id: playlistId, startIndex: 0 },
});
});
const playlistSongIds = api.normalize const playlistSongIds = playlistSongsRes?.items?.map((song) => song.id);
.songList(playlistSongsRes, server)
.items?.map((song) => song.id);
for (const songId of allSongIds) { for (const songId of allSongIds) {
if (!playlistSongIds?.includes(songId)) { if (!playlistSongIds?.includes(songId)) {
@ -138,10 +146,12 @@ export const AddToPlaylistContextModal = ({
} }
if (values.skipDuplicates ? uniqueSongIds.length > 0 : allSongIds.length > 0) { if (values.skipDuplicates ? uniqueSongIds.length > 0 : allSongIds.length > 0) {
if (!server) return null;
addToPlaylistMutation.mutate( addToPlaylistMutation.mutate(
{ {
body: { songId: values.skipDuplicates ? uniqueSongIds : allSongIds }, body: { songId: values.skipDuplicates ? uniqueSongIds : allSongIds },
query: { id: playlistId }, query: { id: playlistId },
serverId: server?.id,
}, },
{ {
onError: (err) => { onError: (err) => {

View file

@ -16,38 +16,43 @@ interface CreatePlaylistFormProps {
} }
export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => { export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
const mutation = useCreatePlaylist(); const mutation = useCreatePlaylist({});
const server = useCurrentServer(); const server = useCurrentServer();
const queryBuilderRef = useRef<PlaylistQueryBuilderRef>(null); const queryBuilderRef = useRef<PlaylistQueryBuilderRef>(null);
const form = useForm<CreatePlaylistBody>({ const form = useForm<CreatePlaylistBody>({
initialValues: { initialValues: {
comment: '', _custom: {
name: '', navidrome: {
ndParams: {
public: false, public: false,
rules: undefined, rules: undefined,
}, },
}, },
comment: '',
name: '',
},
}); });
const [isSmartPlaylist, setIsSmartPlaylist] = useState(false); const [isSmartPlaylist, setIsSmartPlaylist] = useState(false);
const handleSubmit = form.onSubmit((values) => { const handleSubmit = form.onSubmit((values) => {
if (isSmartPlaylist) { if (isSmartPlaylist) {
values.ndParams = { values._custom!.navidrome = {
...values.ndParams, ...values._custom?.navidrome,
rules: queryBuilderRef.current?.getFilters(), rules: queryBuilderRef.current?.getFilters(),
}; };
} }
const smartPlaylist = queryBuilderRef.current?.getFilters(); const smartPlaylist = queryBuilderRef.current?.getFilters();
if (!server) return;
mutation.mutate( mutation.mutate(
{ {
body: { body: {
...values, ...values,
ndParams: { _custom: {
...values.ndParams, navidrome: {
...values._custom?.navidrome,
rules: rules:
isSmartPlaylist && smartPlaylist?.filters isSmartPlaylist && smartPlaylist?.filters
? { ? {
@ -58,6 +63,8 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
}, },
}, },
}, },
serverId: server.id,
},
{ {
onError: (err) => { onError: (err) => {
toast.error({ message: err.message, title: 'Error creating playlist' }); toast.error({ message: err.message, title: 'Error creating playlist' });

View file

@ -17,16 +17,12 @@ import {
UserListQuery, UserListQuery,
UserListSort, UserListSort,
} from '/@/renderer/api/types'; } from '/@/renderer/api/types';
import { Button, ConfirmModal, DropdownMenu, MotionGroup, toast } from '/@/renderer/components';
import { import {
Button,
ConfirmModal,
DropdownMenu,
getColumnDefs, getColumnDefs,
MotionGroup,
toast,
useFixedTableHeader, useFixedTableHeader,
VirtualTable, VirtualTable,
} from '/@/renderer/components'; } from '/@/renderer/components/virtual-table';
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu'; import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
import { import {
PLAYLIST_SONG_CONTEXT_MENU_ITEMS, PLAYLIST_SONG_CONTEXT_MENU_ITEMS,
@ -68,19 +64,23 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
const { playlistId } = useParams() as { playlistId: string }; const { playlistId } = useParams() as { playlistId: string };
const page = useSongListStore(); const page = useSongListStore();
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
const detailQuery = usePlaylistDetail({ id: playlistId }); const server = useCurrentServer();
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const server = useCurrentServer();
const playlistSongsQueryInfinite = usePlaylistSongListInfinite( const playlistSongsQueryInfinite = usePlaylistSongListInfinite({
{ options: {
cacheTime: 0,
keepPreviousData: false,
},
query: {
id: playlistId, id: playlistId,
limit: 50, limit: 50,
startIndex: 0, startIndex: 0,
}, },
{ cacheTime: 0, keepPreviousData: false }, serverId: server?.id,
); });
const handleLoadMore = () => { const handleLoadMore = () => {
playlistSongsQueryInfinite.fetchNextPage(); playlistSongsQueryInfinite.fetchNextPage();
@ -105,17 +105,17 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
}); });
const playlistSongData = useMemo( const playlistSongData = useMemo(
() => playlistSongsQueryInfinite.data?.pages.flatMap((p) => p.items), () => playlistSongsQueryInfinite.data?.pages.flatMap((p) => p?.items),
[playlistSongsQueryInfinite.data?.pages], [playlistSongsQueryInfinite.data?.pages],
); );
const { intersectRef, tableContainerRef } = useFixedTableHeader(); const { intersectRef, tableContainerRef } = useFixedTableHeader();
const deletePlaylistMutation = useDeletePlaylist(); const deletePlaylistMutation = useDeletePlaylist({});
const handleDeletePlaylist = () => { const handleDeletePlaylist = () => {
deletePlaylistMutation.mutate( deletePlaylistMutation.mutate(
{ query: { id: playlistId } }, { query: { id: playlistId }, serverId: server?.id },
{ {
onError: (err) => { onError: (err) => {
toast.error({ toast.error({
@ -165,30 +165,33 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
startIndex: 0, startIndex: 0,
}; };
if (!server) return;
const users = await queryClient.fetchQuery({ 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), queryKey: queryKeys.users.list(server?.id || '', query),
}); });
const normalizedUsers = api.normalize.userList(users, server);
openModal({ openModal({
children: ( children: (
<UpdatePlaylistForm <UpdatePlaylistForm
body={{ body={{
comment: detailQuery?.data?.description || undefined, _custom: {
genres: detailQuery?.data?.genres, navidrome: {
name: detailQuery?.data?.name,
ndParams: {
owner: detailQuery?.data?.owner || undefined, owner: detailQuery?.data?.owner || undefined,
ownerId: detailQuery?.data?.ownerId || undefined, ownerId: detailQuery?.data?.ownerId || undefined,
public: detailQuery?.data?.public || false, public: detailQuery?.data?.public || false,
rules: detailQuery?.data?.rules || undefined, rules: detailQuery?.data?.rules || undefined,
sync: detailQuery?.data?.sync || undefined, sync: detailQuery?.data?.sync || undefined,
}, },
},
comment: detailQuery?.data?.description || undefined,
genres: detailQuery?.data?.genres,
name: detailQuery?.data?.name,
}} }}
query={{ id: playlistId }} query={{ id: playlistId }}
users={normalizedUsers.items} users={users?.items}
onCancel={closeAllModals} onCancel={closeAllModals}
/> />
), ),

View file

@ -7,6 +7,7 @@ import { LibraryHeader } from '/@/renderer/features/shared';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { formatDurationString } from '/@/renderer/utils'; import { formatDurationString } from '/@/renderer/utils';
import { LibraryItem } from '/@/renderer/api/types'; import { LibraryItem } from '/@/renderer/api/types';
import { useCurrentServer } from '../../../store/auth.store';
interface PlaylistDetailHeaderProps { interface PlaylistDetailHeaderProps {
background: string; background: string;
@ -20,7 +21,8 @@ export const PlaylistDetailHeader = forwardRef(
ref: Ref<HTMLDivElement>, ref: Ref<HTMLDivElement>,
) => { ) => {
const { playlistId } = useParams() as { playlistId: string }; 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 = [ const metadataItems = [
{ {

View file

@ -8,12 +8,6 @@ import type {
RowDoubleClickedEvent, RowDoubleClickedEvent,
} from '@ag-grid-community/core'; } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import {
getColumnDefs,
TablePagination,
VirtualGridAutoSizerContainer,
VirtualTable,
} from '/@/renderer/components';
import { import {
useCurrentServer, useCurrentServer,
usePlaylistDetailStore, usePlaylistDetailStore,
@ -44,6 +38,8 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query'; 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 { interface PlaylistDetailContentProps {
tableRef: MutableRefObject<AgGridReactType | null>; tableRef: MutableRefObject<AgGridReactType | null>;
@ -61,7 +57,7 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
}; };
}, [page?.table.id, playlistId]); }, [page?.table.id, playlistId]);
const detailQuery = usePlaylistDetail({ id: playlistId }); const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
const p = usePlaylistDetailTablePagination(playlistId); const p = usePlaylistDetailTablePagination(playlistId);
const pagination = { const pagination = {
@ -80,9 +76,12 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED; const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED;
const checkPlaylistList = usePlaylistSongList({ const checkPlaylistList = usePlaylistSongList({
query: {
id: playlistId, id: playlistId,
limit: 1, limit: 1,
startIndex: 0, startIndex: 0,
},
serverId: server?.id,
}); });
const columnDefs: ColDef[] = useMemo( const columnDefs: ColDef[] = useMemo(
@ -104,24 +103,27 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
...filters, ...filters,
}); });
if (!server) return;
const songsRes = await queryClient.fetchQuery( const songsRes = await queryClient.fetchQuery(
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getPlaylistSongList({ api.controller.getPlaylistSongList({
apiClientProps: {
server,
signal,
},
query: { query: {
id: playlistId, id: playlistId,
limit, limit,
startIndex, startIndex,
...filters, ...filters,
}, },
server,
signal,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );
const songs = api.normalize.songList(songsRes, server); params.successCallback(songsRes?.items || [], songsRes?.totalRecordCount || 0);
params.successCallback(songs?.items || [], songsRes?.totalRecordCount || 0);
}, },
rowCount: undefined, rowCount: undefined,
}; };

View file

@ -18,15 +18,7 @@ import {
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { LibraryItem, PlaylistSongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types'; import { LibraryItem, PlaylistSongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types';
import { import { DropdownMenu, Button, Slider, MultiSelect, Switch, Text } from '/@/renderer/components';
DropdownMenu,
SONG_TABLE_COLUMNS,
Button,
Slider,
MultiSelect,
Switch,
Text,
} from '/@/renderer/components';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { import {
@ -41,6 +33,7 @@ import {
import { ListDisplayType, ServerType, Play, TableColumn } from '/@/renderer/types'; import { ListDisplayType, ServerType, Play, TableColumn } from '/@/renderer/types';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query'; import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
const FILTERS = { const FILTERS = {
jellyfin: [ jellyfin: [
@ -100,7 +93,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC, 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 isSmartPlaylist = detailQuery.data?.rules;
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
@ -139,14 +132,16 @@ export const PlaylistDetailSongListHeaderFilters = ({
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getPlaylistSongList({ api.controller.getPlaylistSongList({
apiClientProps: {
server,
signal,
},
query: { query: {
id: playlistId, id: playlistId,
limit, limit,
startIndex, startIndex,
...filters, ...filters,
}, },
server,
signal,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );

View file

@ -8,6 +8,7 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
import { PlaylistDetailSongListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header-filters'; import { PlaylistDetailSongListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header-filters';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query'; import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { LibraryHeaderBar } from '/@/renderer/features/shared'; import { LibraryHeaderBar } from '/@/renderer/features/shared';
import { useCurrentServer } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { Play } from '/@/renderer/types'; import { Play } from '/@/renderer/types';
@ -23,7 +24,8 @@ export const PlaylistDetailSongListHeader = ({
handleToggleShowQueryBuilder, handleToggleShowQueryBuilder,
}: PlaylistDetailHeaderProps) => { }: PlaylistDetailHeaderProps) => {
const { playlistId } = useParams() as { playlistId: string }; 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 handlePlayQueueAdd = usePlayQueueAdd();
const handlePlay = async (playType: Play) => { const handlePlay = async (playType: Play) => {

View file

@ -12,12 +12,6 @@ import { Stack } from '@mantine/core';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import {
getColumnDefs,
TablePagination,
VirtualGridAutoSizerContainer,
VirtualTable,
} from '/@/renderer/components';
import { import {
useCurrentServer, useCurrentServer,
usePlaylistListStore, usePlaylistListStore,
@ -33,6 +27,8 @@ import { PLAYLIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/c
import { generatePath, useNavigate } from 'react-router'; import { generatePath, useNavigate } from 'react-router';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { LibraryItem } from '/@/renderer/api/types'; import { LibraryItem } from '/@/renderer/api/types';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
import { getColumnDefs, VirtualTable, TablePagination } from '/@/renderer/components/virtual-table';
interface PlaylistListContentProps { interface PlaylistListContentProps {
itemCount?: number; itemCount?: number;
@ -81,13 +77,15 @@ export const PlaylistListContent = ({ tableRef, itemCount }: PlaylistListContent
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getPlaylistList({ api.controller.getPlaylistList({
apiClientProps: {
server,
signal,
},
query: { query: {
limit, limit,
startIndex, startIndex,
...page.filter, ...page.filter,
}, },
server,
signal,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );

View file

@ -7,15 +7,7 @@ import { RiSortAsc, RiSortDesc, RiMoreFill, RiRefreshLine, RiSettings3Fill } fro
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { SortOrder, PlaylistListSort } from '/@/renderer/api/types'; import { SortOrder, PlaylistListSort } from '/@/renderer/api/types';
import { import { DropdownMenu, Text, Button, Slider, MultiSelect, Switch } from '/@/renderer/components';
DropdownMenu,
PLAYLIST_TABLE_COLUMNS,
Text,
Button,
Slider,
MultiSelect,
Switch,
} from '/@/renderer/components';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { import {
PlaylistListFilter, PlaylistListFilter,
@ -27,6 +19,7 @@ import {
useSetPlaylistTablePagination, useSetPlaylistTablePagination,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { ListDisplayType, TableColumn } from '/@/renderer/types'; import { ListDisplayType, TableColumn } from '/@/renderer/types';
import { PLAYLIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
const FILTERS = { const FILTERS = {
jellyfin: [ jellyfin: [
@ -91,13 +84,15 @@ export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFilter
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getPlaylistList({ api.controller.getPlaylistList({
apiClientProps: {
server,
signal,
},
query: { query: {
limit, limit,
startIndex, startIndex,
...pageFilters, ...pageFilters,
}, },
server,
signal,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );

View file

@ -1,6 +1,6 @@
import { Group, Stack } from '@mantine/core'; import { Group, Stack } from '@mantine/core';
import { useForm } from '@mantine/form'; 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 { Button, Switch, TextInput, toast } from '/@/renderer/components';
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation'; import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
@ -8,23 +8,25 @@ import { useCurrentServer } from '/@/renderer/store';
interface SaveAsPlaylistFormProps { interface SaveAsPlaylistFormProps {
body: Partial<CreatePlaylistBody>; body: Partial<CreatePlaylistBody>;
onCancel: () => void; onCancel: () => void;
onSuccess: (data: RawCreatePlaylistResponse) => void; onSuccess: (data: CreatePlaylistResponse) => void;
} }
export const SaveAsPlaylistForm = ({ body, onSuccess, onCancel }: SaveAsPlaylistFormProps) => { export const SaveAsPlaylistForm = ({ body, onSuccess, onCancel }: SaveAsPlaylistFormProps) => {
const mutation = useCreatePlaylist(); const mutation = useCreatePlaylist({});
const server = useCurrentServer(); const server = useCurrentServer();
const form = useForm<CreatePlaylistBody>({ const form = useForm<CreatePlaylistBody>({
initialValues: { initialValues: {
comment: body.comment || '', _custom: {
name: body.name || '', navidrome: {
ndParams: {
public: false, public: false,
rules: undefined, rules: undefined,
...body.ndParams, ...body?._custom?.navidrome,
}, },
}, },
comment: body.comment || '',
name: body.name || '',
},
}); });
const handleSubmit = form.onSubmit((values) => { const handleSubmit = form.onSubmit((values) => {

View file

@ -13,7 +13,7 @@ interface UpdatePlaylistFormProps {
} }
export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlaylistFormProps) => { export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlaylistFormProps) => {
const mutation = useUpdatePlaylist(); const mutation = useUpdatePlaylist({});
const server = useCurrentServer(); const server = useCurrentServer();
const userList = users?.map((user) => ({ const userList = users?.map((user) => ({
@ -23,21 +23,27 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
const form = useForm<UpdatePlaylistBody>({ const form = useForm<UpdatePlaylistBody>({
initialValues: { 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 || '', comment: body?.comment || '',
name: body?.name || '', 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) => { const handleSubmit = form.onSubmit((values) => {
mutation.mutate( mutation.mutate(
{ body: values, query }, {
body: values,
query,
serverId: server?.id,
},
{ {
onError: (err) => { onError: (err) => {
toast.error({ message: err.message, title: 'Error updating playlist' }); toast.error({ message: err.message, title: 'Error updating playlist' });

View file

@ -1,5 +1,5 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useRef } from 'react'; import { useRef } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { LibraryItem } from '/@/renderer/api/types'; import { LibraryItem } from '/@/renderer/api/types';
import { NativeScrollArea } from '/@/renderer/components'; 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 { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared';
import { useFastAverageColor } from '/@/renderer/hooks'; import { useFastAverageColor } from '/@/renderer/hooks';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { useCurrentServer } from '../../../store/auth.store';
const PlaylistDetailRoute = () => { const PlaylistDetailRoute = () => {
const tableRef = useRef<AgGridReactType | null>(null); const tableRef = useRef<AgGridReactType | null>(null);
const scrollAreaRef = useRef<HTMLDivElement>(null); const scrollAreaRef = useRef<HTMLDivElement>(null);
const headerRef = useRef<HTMLDivElement>(null); const headerRef = useRef<HTMLDivElement>(null);
const { playlistId } = useParams() as { playlistId: string }; 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( const background = useFastAverageColor(
detailQuery?.data?.imageUrl, detailQuery?.data?.imageUrl,
!detailQuery?.isLoading, !detailQuery?.isLoading,

View file

@ -23,11 +23,11 @@ const PlaylistDetailSongListRoute = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const tableRef = useRef<AgGridReactType | null>(null); const tableRef = useRef<AgGridReactType | null>(null);
const { playlistId } = useParams() as { playlistId: string }; const { playlistId } = useParams() as { playlistId: string };
const currentServer = useCurrentServer(); const server = useCurrentServer();
const detailQuery = usePlaylistDetail({ id: playlistId }); const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
const createPlaylistMutation = useCreatePlaylist(); const createPlaylistMutation = useCreatePlaylist({});
const deletePlaylistMutation = useDeletePlaylist(); const deletePlaylistMutation = useDeletePlaylist({});
const handleSave = ( const handleSave = (
filter: Record<string, any>, filter: Record<string, any>,
@ -45,9 +45,8 @@ const PlaylistDetailSongListRoute = () => {
createPlaylistMutation.mutate( createPlaylistMutation.mutate(
{ {
body: { body: {
comment: detailQuery?.data?.description || '', _custom: {
name: detailQuery?.data?.name, navidrome: {
ndParams: {
owner: detailQuery?.data?.owner || '', owner: detailQuery?.data?.owner || '',
ownerId: detailQuery?.data?.ownerId || '', ownerId: detailQuery?.data?.ownerId || '',
public: detailQuery?.data?.public || false, public: detailQuery?.data?.public || false,
@ -55,6 +54,9 @@ const PlaylistDetailSongListRoute = () => {
sync: detailQuery?.data?.sync || false, sync: detailQuery?.data?.sync || false,
}, },
}, },
comment: detailQuery?.data?.description || '',
name: detailQuery?.data?.name,
},
}, },
{ {
onSuccess: (data) => { onSuccess: (data) => {
@ -73,9 +75,8 @@ const PlaylistDetailSongListRoute = () => {
children: ( children: (
<SaveAsPlaylistForm <SaveAsPlaylistForm
body={{ body={{
comment: detailQuery?.data?.description || '', _custom: {
name: detailQuery?.data?.name, navidrome: {
ndParams: {
owner: detailQuery?.data?.owner || '', owner: detailQuery?.data?.owner || '',
ownerId: detailQuery?.data?.ownerId || '', ownerId: detailQuery?.data?.ownerId || '',
public: detailQuery?.data?.public || false, public: detailQuery?.data?.public || false,
@ -86,6 +87,9 @@ const PlaylistDetailSongListRoute = () => {
}, },
sync: detailQuery?.data?.sync || false, sync: detailQuery?.data?.sync || false,
}, },
},
comment: detailQuery?.data?.description || '',
name: detailQuery?.data?.name,
}} }}
onCancel={closeAllModals} onCancel={closeAllModals}
onSuccess={(data) => onSuccess={(data) =>
@ -120,9 +124,7 @@ const PlaylistDetailSongListRoute = () => {
}; };
const isSmartPlaylist = const isSmartPlaylist =
!detailQuery?.isLoading && !detailQuery?.isLoading && detailQuery?.data?.rules && server?.type === ServerType.NAVIDROME;
detailQuery?.data?.rules &&
currentServer?.type === ServerType.NAVIDROME;
const [showQueryBuilder, setShowQueryBuilder] = useState(false); const [showQueryBuilder, setShowQueryBuilder] = useState(false);
const [isQueryBuilderExpanded, setIsQueryBuilderExpanded] = useState(false); const [isQueryBuilderExpanded, setIsQueryBuilderExpanded] = useState(false);
@ -142,18 +144,19 @@ const PlaylistDetailSongListRoute = () => {
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC, 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, id: playlistId,
limit: 1, limit: 1,
startIndex: 0, startIndex: 0,
...filters, ...filters,
}, },
{ serverId: server?.id,
cacheTime: 1000 * 60 * 60 * 2, });
staleTime: 1000 * 60 * 60 * 2,
},
);
const itemCount = const itemCount =
itemCountCheck.data?.totalRecordCount === null itemCountCheck.data?.totalRecordCount === null

View file

@ -9,18 +9,18 @@ import { AnimatedPage } from '/@/renderer/features/shared';
const PlaylistListRoute = () => { const PlaylistListRoute = () => {
const tableRef = useRef<AgGridReactType | null>(null); const tableRef = useRef<AgGridReactType | null>(null);
const itemCountCheck = usePlaylistList( const itemCountCheck = usePlaylistList({
{ options: {
cacheTime: 1000 * 60 * 60 * 2,
staleTime: 1000 * 60 * 60 * 2,
},
query: {
limit: 1, limit: 1,
sortBy: PlaylistListSort.NAME, sortBy: PlaylistListSort.NAME,
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
startIndex: 0, startIndex: 0,
}, },
{ });
cacheTime: 1000 * 60 * 60 * 2,
staleTime: 1000 * 60 * 60 * 2,
},
);
const itemCount = const itemCount =
itemCountCheck.data?.totalRecordCount === null itemCountCheck.data?.totalRecordCount === null

View file

@ -77,7 +77,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
setCurrentServer(serverItem); setCurrentServer(serverItem);
closeAllModals(); closeAllModals();
if (serverList.length === 0) { if (Object.keys(serverList).length === 0) {
toast.success({ message: 'Server has been added, reloading...' }); toast.success({ message: 'Server has been added, reloading...' });
setTimeout(() => window.location.reload(), 2000); setTimeout(() => window.location.reload(), 2000);
} else { } else {

View file

@ -62,6 +62,7 @@ export const ServerList = () => {
position: 'absolute', position: 'absolute',
right: 55, right: 55,
transform: 'translateY(-3.5rem)', transform: 'translateY(-3.5rem)',
zIndex: 2000,
}} }}
> >
<Button <Button
@ -77,21 +78,24 @@ export const ServerList = () => {
</Group> </Group>
<Stack> <Stack>
<Accordion variant="separated"> <Accordion variant="separated">
{serverListQuery?.map((s) => ( {Object.keys(serverListQuery)?.map((serverId) => {
const server = serverListQuery[serverId];
return (
<Accordion.Item <Accordion.Item
key={s.id} key={server.id}
value={s.name} value={server.name}
> >
<Accordion.Control icon={<RiServerFill size={15} />}> <Accordion.Control icon={<RiServerFill size={15} />}>
<Group position="apart"> <Group position="apart">
{titleCase(s.type)} - {s.name} {titleCase(server?.type)} - {server?.name}
</Group> </Group>
</Accordion.Control> </Accordion.Control>
<Accordion.Panel> <Accordion.Panel>
<ServerListItem server={s} /> <ServerListItem server={server} />
</Accordion.Panel> </Accordion.Panel>
</Accordion.Item> </Accordion.Item>
))} );
})}
</Accordion> </Accordion>
<Divider /> <Divider />
<Group> <Group>

View file

@ -6,5 +6,5 @@ export * from './components/library-header';
export * from './components/library-header-bar'; export * from './components/library-header-bar';
export * from './mutations/create-favorite-mutation'; export * from './mutations/create-favorite-mutation';
export * from './mutations/delete-favorite-mutation'; export * from './mutations/delete-favorite-mutation';
export * from './mutations/update-rating-mutation'; export * from './mutations/set-rating-mutation';
export * from './components/filter-bar'; export * from './components/filter-bar';

View file

@ -100,9 +100,12 @@ export const Sidebar = () => {
}; };
const playlistsQuery = usePlaylistList({ const playlistsQuery = usePlaylistList({
query: {
sortBy: PlaylistListSort.NAME, sortBy: PlaylistListSort.NAME,
sortOrder: SortOrder.ASC, sortOrder: SortOrder.ASC,
startIndex: 0, startIndex: 0,
},
serverId: server?.id,
}); });
const setFullScreenPlayerStore = useSetFullScreenPlayerStore(); const setFullScreenPlayerStore = useSetFullScreenPlayerStore();

View file

@ -9,30 +9,32 @@ interface JellyfinSongFiltersProps {
handleFilterChange: (filters: SongListFilter) => void; handleFilterChange: (filters: SongListFilter) => void;
id?: string; id?: string;
pageKey: string; pageKey: string;
serverId?: string;
} }
export const JellyfinSongFilters = ({ export const JellyfinSongFilters = ({
id, id,
pageKey, pageKey,
handleFilterChange, handleFilterChange,
serverId,
}: JellyfinSongFiltersProps) => { }: JellyfinSongFiltersProps) => {
const { setFilter } = useListStoreActions(); const { setFilter } = useListStoreActions();
const filter = useSongListFilter({ id, key: pageKey }); const filter = useSongListFilter({ id, key: pageKey });
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library // 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(() => { const genreList = useMemo(() => {
if (!genreListQuery?.data) return []; if (!genreListQuery?.data) return [];
return genreListQuery.data.map((genre) => ({ return genreListQuery.data.items.map((genre) => ({
label: genre.name, label: genre.name,
value: genre.id, value: genre.id,
})); }));
}, [genreListQuery.data]); }, [genreListQuery.data]);
const selectedGenres = useMemo(() => { const selectedGenres = useMemo(() => {
return filter.jfParams?.genreIds?.split(','); return filter._custom?.jellyfin?.genreIds?.split(',');
}, [filter.jfParams?.genreIds]); }, [filter._custom?.jellyfin?.genreIds]);
const toggleFilters = [ const toggleFilters = [
{ {
@ -40,17 +42,20 @@ export const JellyfinSongFilters = ({
onChange: (e: ChangeEvent<HTMLInputElement>) => { onChange: (e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
jfParams: { _custom: {
...filter.jfParams, ...filter._custom,
jellyfin: {
...filter._custom?.jellyfin,
includeItemTypes: 'Audio', includeItemTypes: 'Audio',
isFavorite: e.currentTarget.checked ? true : undefined, isFavorite: e.currentTarget.checked ? true : undefined,
}, },
}, },
},
key: pageKey, key: pageKey,
}) as SongListFilter; }) as SongListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
}, },
value: filter.jfParams?.isFavorite, value: filter._custom?.jellyfin?.isFavorite,
}, },
]; ];
@ -58,12 +63,15 @@ export const JellyfinSongFilters = ({
if (typeof e === 'number' && (e < 1700 || e > 2300)) return; if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
jfParams: { _custom: {
...filter.jfParams, ...filter._custom,
jellyfin: {
...filter._custom?.jellyfin,
includeItemTypes: 'Audio', includeItemTypes: 'Audio',
minYear: e === '' ? undefined : (e as number), minYear: e === '' ? undefined : (e as number),
}, },
}, },
},
key: pageKey, key: pageKey,
}) as SongListFilter; }) as SongListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
@ -73,12 +81,15 @@ export const JellyfinSongFilters = ({
if (typeof e === 'number' && (e < 1700 || e > 2300)) return; if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
jfParams: { _custom: {
...filter.jfParams, ...filter._custom,
jellyfin: {
...filter._custom?.jellyfin,
includeItemTypes: 'Audio', includeItemTypes: 'Audio',
maxYear: e === '' ? undefined : (e as number), maxYear: e === '' ? undefined : (e as number),
}, },
}, },
},
key: pageKey, key: pageKey,
}) as SongListFilter; }) as SongListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
@ -88,12 +99,15 @@ export const JellyfinSongFilters = ({
const genreFilterString = e?.length ? e.join(',') : undefined; const genreFilterString = e?.length ? e.join(',') : undefined;
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
jfParams: { _custom: {
...filter.jfParams, ...filter._custom,
jellyfin: {
...filter._custom?.jellyfin,
genreIds: genreFilterString, genreIds: genreFilterString,
includeItemTypes: 'Audio', includeItemTypes: 'Audio',
}, },
}, },
},
key: pageKey, key: pageKey,
}) as SongListFilter; }) as SongListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
@ -117,14 +131,14 @@ export const JellyfinSongFilters = ({
<Group grow> <Group grow>
<NumberInput <NumberInput
required required
defaultValue={filter.jfParams?.minYear} defaultValue={filter._custom?.jellyfin?.minYear}
label="From year" label="From year"
max={2300} max={2300}
min={1700} min={1700}
onChange={handleMinYearFilter} onChange={handleMinYearFilter}
/> />
<NumberInput <NumberInput
defaultValue={filter.jfParams?.maxYear} defaultValue={filter._custom?.jellyfin?.maxYear}
label="To year" label="To year"
max={2300} max={2300}
min={1700} min={1700}

View file

@ -9,21 +9,23 @@ interface NavidromeSongFiltersProps {
handleFilterChange: (filters: SongListFilter) => void; handleFilterChange: (filters: SongListFilter) => void;
id?: string; id?: string;
pageKey: string; pageKey: string;
serverId?: string;
} }
export const NavidromeSongFilters = ({ export const NavidromeSongFilters = ({
handleFilterChange, handleFilterChange,
pageKey, pageKey,
id, id,
serverId,
}: NavidromeSongFiltersProps) => { }: NavidromeSongFiltersProps) => {
const { setFilter } = useListStoreActions(); const { setFilter } = useListStoreActions();
const filter = useSongListFilter({ id, key: pageKey }); const filter = useSongListFilter({ id, key: pageKey });
const genreListQuery = useGenreList(null); const genreListQuery = useGenreList({ query: null, serverId });
const genreList = useMemo(() => { const genreList = useMemo(() => {
if (!genreListQuery?.data) return []; if (!genreListQuery?.data) return [];
return genreListQuery.data.map((genre) => ({ return genreListQuery.data.items.map((genre) => ({
label: genre.name, label: genre.name,
value: genre.id, value: genre.id,
})); }));
@ -32,11 +34,13 @@ export const NavidromeSongFilters = ({
const handleGenresFilter = debounce((e: string | null) => { const handleGenresFilter = debounce((e: string | null) => {
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
ndParams: { _custom: {
...filter.ndParams, ...filter._custom,
navidrome: {
genre_id: e || undefined, genre_id: e || undefined,
}, },
}, },
},
key: pageKey, key: pageKey,
}) as SongListFilter; }) as SongListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
@ -48,25 +52,32 @@ export const NavidromeSongFilters = ({
onChange: (e: ChangeEvent<HTMLInputElement>) => { onChange: (e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
ndParams: { ...filter.ndParams, starred: e.currentTarget.checked ? true : undefined }, _custom: {
...filter._custom,
navidrome: {
starred: e.currentTarget.checked ? true : undefined,
},
},
}, },
key: pageKey, key: pageKey,
}) as SongListFilter; }) as SongListFilter;
handleFilterChange(updatedFilters); handleFilterChange(updatedFilters);
}, },
value: filter.ndParams?.starred, value: filter._custom?.navidrome?.starred,
}, },
]; ];
const handleYearFilter = debounce((e: number | string) => { const handleYearFilter = debounce((e: number | string) => {
const updatedFilters = setFilter({ const updatedFilters = setFilter({
data: { data: {
ndParams: { _custom: {
...filter.ndParams, ...filter._custom,
navidrome: {
year: e === '' ? undefined : (e as number), year: e === '' ? undefined : (e as number),
}, },
}, },
},
key: pageKey, key: pageKey,
}) as SongListFilter; }) as SongListFilter;
@ -94,7 +105,7 @@ export const NavidromeSongFilters = ({
label="Year" label="Year"
max={5000} max={5000}
min={0} min={0}
value={filter.ndParams?.year} value={filter._custom?.navidrome?.year}
width={50} width={50}
onChange={(e) => handleYearFilter(e)} onChange={(e) => handleYearFilter(e)}
/> />
@ -102,7 +113,7 @@ export const NavidromeSongFilters = ({
clearable clearable
searchable searchable
data={genreList} data={genreList}
defaultValue={filter.ndParams?.genre_id} defaultValue={filter._custom?.navidrome?.genre_id}
label="Genre" label="Genre"
width={150} width={150}
onChange={handleGenresFilter} onChange={handleGenresFilter}

View file

@ -12,12 +12,6 @@ import { Stack } from '@mantine/core';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import {
getColumnDefs,
TablePagination,
VirtualGridAutoSizerContainer,
VirtualTable,
} from '/@/renderer/components';
import { import {
useCurrentServer, useCurrentServer,
useListStoreActions, useListStoreActions,
@ -33,6 +27,8 @@ import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { LibraryItem, QueueSong, SongListQuery } from '/@/renderer/api/types'; import { LibraryItem, QueueSong, SongListQuery } from '/@/renderer/api/types';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { useSongListContext } from '/@/renderer/features/songs/context/song-list-context'; import { useSongListContext } from '/@/renderer/features/songs/context/song-list-context';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
import { getColumnDefs, VirtualTable, TablePagination } from '/@/renderer/components/virtual-table';
interface SongListContentProps { interface SongListContentProps {
itemCount?: number; itemCount?: number;
@ -74,9 +70,11 @@ export const SongListContent = ({ itemCount, tableRef }: SongListContentProps) =
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getSongList({ api.controller.getSongList({
query, apiClientProps: {
server, server,
signal, signal,
},
query,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );

View file

@ -17,15 +17,7 @@ import {
import { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { LibraryItem, SongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types'; import { LibraryItem, SongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types';
import { import { DropdownMenu, Button, Slider, MultiSelect, Switch, Text } from '/@/renderer/components';
DropdownMenu,
SONG_TABLE_COLUMNS,
Button,
Slider,
MultiSelect,
Switch,
Text,
} from '/@/renderer/components';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { useMusicFolders } from '/@/renderer/features/shared'; import { useMusicFolders } from '/@/renderer/features/shared';
import { JellyfinSongFilters } from '/@/renderer/features/songs/components/jellyfin-song-filters'; import { JellyfinSongFilters } from '/@/renderer/features/songs/components/jellyfin-song-filters';
@ -42,6 +34,7 @@ import {
} from '/@/renderer/store'; } from '/@/renderer/store';
import { ListDisplayType, ServerType, Play, TableColumn } from '/@/renderer/types'; import { ListDisplayType, ServerType, Play, TableColumn } from '/@/renderer/types';
import { useSongListContext } from '/@/renderer/features/songs/context/song-list-context'; import { useSongListContext } from '/@/renderer/features/songs/context/song-list-context';
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
const FILTERS = { const FILTERS = {
jellyfin: [ jellyfin: [
@ -100,7 +93,7 @@ export const SongListHeaderFilters = ({
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
const cq = useContainerQuery(); const cq = useContainerQuery();
const musicFoldersQuery = useMusicFolders(); const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id });
const sortByLabel = const sortByLabel =
(server?.type && (server?.type &&
@ -133,9 +126,11 @@ export const SongListHeaderFilters = ({
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getSongList({ api.controller.getSongList({
query, apiClientProps: {
server, server,
signal, signal,
},
query,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );
@ -306,18 +301,18 @@ export const SongListHeaderFilters = ({
const isFilterApplied = useMemo(() => { const isFilterApplied = useMemo(() => {
const isNavidromeFilterApplied = const isNavidromeFilterApplied =
server?.type === ServerType.NAVIDROME && server?.type === ServerType.NAVIDROME &&
filter.ndParams && filter._custom?.navidrome &&
Object.values(filter.ndParams).some((value) => value !== undefined); Object.values(filter._custom?.navidrome).some((value) => value !== undefined);
const isJellyfinFilterApplied = const isJellyfinFilterApplied =
server?.type === ServerType.JELLYFIN && server?.type === ServerType.JELLYFIN &&
filter.jfParams && filter._custom?.jellyfin &&
Object.values(filter.jfParams) Object.values(filter._custom?.jellyfin)
.filter((value) => value !== 'Audio') // Don't account for includeItemTypes: Audio .filter((value) => value !== 'Audio') // Don't account for includeItemTypes: Audio
.some((value) => value !== undefined); .some((value) => value !== undefined);
return isNavidromeFilterApplied || isJellyfinFilterApplied; return isNavidromeFilterApplied || isJellyfinFilterApplied;
}, [filter.jfParams, filter.ndParams, server?.type]); }, [filter._custom?.jellyfin, filter._custom?.navidrome, server?.type]);
return ( return (
<Flex justify="space-between"> <Flex justify="space-between">
@ -382,7 +377,7 @@ export const SongListHeaderFilters = ({
</Button> </Button>
</DropdownMenu.Target> </DropdownMenu.Target>
<DropdownMenu.Dropdown> <DropdownMenu.Dropdown>
{musicFoldersQuery.data?.map((folder) => ( {musicFoldersQuery.data?.items.map((folder) => (
<DropdownMenu.Item <DropdownMenu.Item
key={`musicFolder-${folder.id}`} key={`musicFolder-${folder.id}`}
$isActive={filter.musicFolderId === folder.id} $isActive={filter.musicFolderId === folder.id}

View file

@ -64,15 +64,16 @@ export const SongListHeader = ({
queryKey, queryKey,
async ({ signal }) => async ({ signal }) =>
api.controller.getSongList({ api.controller.getSongList({
query, apiClientProps: {
server, server,
signal, signal,
},
query,
}), }),
{ cacheTime: 1000 * 60 * 1 }, { cacheTime: 1000 * 60 * 1 },
); );
const songs = api.normalize.songList(songsRes, server); params.successCallback(songsRes?.items || [], songsRes?.totalRecordCount || 0);
params.successCallback(songs?.items || [], songsRes?.totalRecordCount || 0);
}, },
rowCount: undefined, rowCount: undefined,
}; };

View file

@ -19,17 +19,18 @@ const TrackListRoute = () => {
); );
const songListFilter = useSongListFilter({ id: albumArtistId, key: pageKey }); const songListFilter = useSongListFilter({ id: albumArtistId, key: pageKey });
const itemCountCheck = useSongList( const itemCountCheck = useSongList({
{ options: {
cacheTime: 1000 * 60,
staleTime: 1000 * 60,
},
query: {
limit: 1, limit: 1,
startIndex: 0, startIndex: 0,
...songListFilter, ...songListFilter,
}, },
{ serverId: server?.id,
cacheTime: 1000 * 60, });
staleTime: 1000 * 60,
},
);
const itemCount = const itemCount =
itemCountCheck.data?.totalRecordCount === null itemCountCheck.data?.totalRecordCount === null

View file

@ -60,22 +60,23 @@ export const AppMenu = () => {
return ( return (
<> <>
<DropdownMenu.Label>Select a server</DropdownMenu.Label> <DropdownMenu.Label>Select a server</DropdownMenu.Label>
{serverList.map((s) => { {Object.keys(serverList).map((serverId) => {
const isNavidromeExpired = s.type === ServerType.NAVIDROME && !s.ndCredential; const server = serverList[serverId];
const isNavidromeExpired = server.type === ServerType.NAVIDROME && !server.ndCredential;
const isJellyfinExpired = false; const isJellyfinExpired = false;
const isSessionExpired = isNavidromeExpired || isJellyfinExpired; const isSessionExpired = isNavidromeExpired || isJellyfinExpired;
return ( return (
<DropdownMenu.Item <DropdownMenu.Item
key={`server-${s.id}`} key={`server-${server.id}`}
$isActive={s.id === currentServer?.id} $isActive={server.id === currentServer?.id}
icon={isSessionExpired ? <RiLockLine color="var(--danger-color)" /> : <RiServerFill />} icon={isSessionExpired ? <RiLockLine color="var(--danger-color)" /> : <RiServerFill />}
onClick={() => { onClick={() => {
if (!isSessionExpired) return handleSetCurrentServer(s); if (!isSessionExpired) return handleSetCurrentServer(server);
return handleCredentialsModal(s); return handleCredentialsModal(server);
}} }}
> >
<Group>{s.name}</Group> <Group>{server.name}</Group>
</DropdownMenu.Item> </DropdownMenu.Item>
); );
})} })}