Refactor all api instances in components
This commit is contained in:
parent
bdd023fde3
commit
314bd766df
56 changed files with 879 additions and 755 deletions
|
@ -27,8 +27,6 @@ export * from './text';
|
|||
export * from './text-title';
|
||||
export * from './toast';
|
||||
export * from './tooltip';
|
||||
export * from './virtual-grid';
|
||||
export * from './virtual-table';
|
||||
export * from './motion';
|
||||
export * from './context-menu';
|
||||
export * from './query-builder';
|
||||
|
|
|
@ -1,50 +1,13 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
import { RiHeartFill, RiHeartLine } from 'react-icons/ri';
|
||||
import { Button } from '/@/renderer/components/button';
|
||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { HTTPError } from 'ky';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { RawFavoriteResponse, FavoriteArgs, LibraryItem } from '/@/renderer/api/types';
|
||||
import { useCurrentServer, useSetAlbumListItemDataById } from '/@/renderer/store';
|
||||
|
||||
const useCreateFavorite = () => {
|
||||
const server = useCurrentServer();
|
||||
const setAlbumListData = useSetAlbumListItemDataById();
|
||||
|
||||
return useMutation<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 });
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
|
||||
|
||||
export const FavoriteCell = ({ value, data, node }: ICellRendererParams) => {
|
||||
const createMutation = useCreateFavorite();
|
||||
const deleteMutation = useDeleteFavorite();
|
||||
const createMutation = useCreateFavorite({});
|
||||
const deleteMutation = useDeleteFavorite({});
|
||||
|
||||
const handleToggleFavorite = () => {
|
||||
const newFavoriteValue = !value;
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import { MouseEvent } from 'react';
|
||||
import type { ICellRendererParams } from '@ag-grid-community/core';
|
||||
import { Rating } from '/@/renderer/components/rating';
|
||||
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
|
||||
import { useUpdateRating } from '/@/renderer/components/virtual-table/hooks/use-rating';
|
||||
import { useSetRating } from '/@/renderer/features/shared';
|
||||
|
||||
export const RatingCell = ({ value, node }: ICellRendererParams) => {
|
||||
const updateRatingMutation = useUpdateRating();
|
||||
const updateRatingMutation = useSetRating({});
|
||||
|
||||
const handleUpdateRating = (rating: number) => {
|
||||
if (!value) return;
|
||||
|
||||
updateRatingMutation.mutate(
|
||||
{
|
||||
_serverId: value?.serverId,
|
||||
query: {
|
||||
item: [value],
|
||||
rating,
|
||||
},
|
||||
serverId: value?.serverId,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
@ -31,11 +32,11 @@ export const RatingCell = ({ value, node }: ICellRendererParams) => {
|
|||
e.stopPropagation();
|
||||
updateRatingMutation.mutate(
|
||||
{
|
||||
_serverId: value?.serverId,
|
||||
query: {
|
||||
item: [value],
|
||||
rating: 0,
|
||||
},
|
||||
serverId: value?.serverId,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import { Ref, forwardRef, useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import type {
|
||||
ICellRendererParams,
|
||||
|
@ -28,8 +29,8 @@ import { GenericTableHeader } from '/@/renderer/components/virtual-table/headers
|
|||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { PersistedTableColumn } from '/@/renderer/store/settings.store';
|
||||
import { TableColumn } from '/@/renderer/types';
|
||||
import { RatingCell } from '/@/renderer/components/virtual-table/cells/rating-cell';
|
||||
import { FavoriteCell } from '/@/renderer/components/virtual-table/cells/favorite-cell';
|
||||
import { RatingCell } from '/@/renderer/components/virtual-table/cells/rating-cell';
|
||||
|
||||
export * from './table-config-dropdown';
|
||||
export * from './table-pagination';
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
import { MutableRefObject, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
Button,
|
||||
getColumnDefs,
|
||||
GridCarousel,
|
||||
Text,
|
||||
TextTitle,
|
||||
useFixedTableHeader,
|
||||
VirtualTable,
|
||||
} from '/@/renderer/components';
|
||||
import { Button, GridCarousel, Text, TextTitle } from '/@/renderer/components';
|
||||
import { ColDef, RowDoubleClickedEvent, RowHeightParams, RowNode } from '@ag-grid-community/core';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { Box, Group, Stack } from '@mantine/core';
|
||||
|
@ -33,6 +25,12 @@ import { PlayButton, useCreateFavorite, useDeleteFavorite } from '/@/renderer/fe
|
|||
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
|
||||
import { AlbumListSort, LibraryItem, QueueSong, SortOrder } from '/@/renderer/api/types';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import {
|
||||
getColumnDefs,
|
||||
useFixedTableHeader,
|
||||
VirtualTable,
|
||||
} from '/@/renderer/components/virtual-table';
|
||||
|
||||
const isFullWidthRow = (node: RowNode) => {
|
||||
return node.id?.includes('disc-');
|
||||
|
@ -60,7 +58,8 @@ interface AlbumDetailContentProps {
|
|||
|
||||
export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => {
|
||||
const { albumId } = useParams() as { albumId: string };
|
||||
const detailQuery = useAlbumDetail({ id: albumId });
|
||||
const server = useCurrentServer();
|
||||
const detailQuery = useAlbumDetail({ query: { id: albumId }, serverId: server?.id });
|
||||
const cq = useContainerQuery();
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
|
||||
|
@ -165,26 +164,29 @@ export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => {
|
|||
|
||||
const itemsPerPage = cq.isXl ? 9 : cq.isLg ? 7 : cq.isMd ? 5 : cq.isSm ? 4 : 3;
|
||||
|
||||
const artistQuery = useAlbumList(
|
||||
{
|
||||
jfParams: {
|
||||
albumArtistIds: detailQuery?.data?.albumArtists[0]?.id,
|
||||
},
|
||||
limit: itemsPerPage,
|
||||
ndParams: {
|
||||
artist_id: detailQuery?.data?.albumArtists[0]?.id,
|
||||
},
|
||||
sortBy: AlbumListSort.YEAR,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: pagination.artist * itemsPerPage,
|
||||
},
|
||||
{
|
||||
const artistQuery = useAlbumList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60,
|
||||
enabled: detailQuery?.data?.albumArtists[0]?.id !== undefined,
|
||||
keepPreviousData: true,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
);
|
||||
query: {
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
albumArtistIds: detailQuery?.data?.albumArtists[0]?.id,
|
||||
},
|
||||
navidrome: {
|
||||
artist_id: detailQuery?.data?.albumArtists[0]?.id,
|
||||
},
|
||||
},
|
||||
limit: itemsPerPage,
|
||||
sortBy: AlbumListSort.YEAR,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: pagination.artist * itemsPerPage,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const carousels = [
|
||||
{
|
||||
|
@ -227,8 +229,8 @@ export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => {
|
|||
});
|
||||
};
|
||||
|
||||
const createFavoriteMutation = useCreateFavorite();
|
||||
const deleteFavoriteMutation = useDeleteFavorite();
|
||||
const createFavoriteMutation = useCreateFavorite({});
|
||||
const deleteFavoriteMutation = useDeleteFavorite({});
|
||||
|
||||
const handleFavorite = () => {
|
||||
if (!detailQuery?.data) return;
|
||||
|
|
|
@ -5,9 +5,10 @@ import { Link } from 'react-router-dom';
|
|||
import { LibraryItem, ServerType } from '/@/renderer/api/types';
|
||||
import { Button, Rating, Text } from '/@/renderer/components';
|
||||
import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query';
|
||||
import { LibraryHeader, useUpdateRating } from '/@/renderer/features/shared';
|
||||
import { LibraryHeader, useSetRating } from '/@/renderer/features/shared';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { formatDurationString } from '/@/renderer/utils';
|
||||
|
||||
interface AlbumDetailHeaderProps {
|
||||
|
@ -17,7 +18,8 @@ interface AlbumDetailHeaderProps {
|
|||
export const AlbumDetailHeader = forwardRef(
|
||||
({ background }: AlbumDetailHeaderProps, ref: Ref<HTMLDivElement>) => {
|
||||
const { albumId } = useParams() as { albumId: string };
|
||||
const detailQuery = useAlbumDetail({ id: albumId });
|
||||
const server = useCurrentServer();
|
||||
const detailQuery = useAlbumDetail({ query: { id: albumId }, serverId: server?.id });
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const metadataItems = [
|
||||
|
@ -38,17 +40,17 @@ export const AlbumDetailHeader = forwardRef(
|
|||
},
|
||||
];
|
||||
|
||||
const updateRatingMutation = useUpdateRating();
|
||||
const updateRatingMutation = useSetRating({});
|
||||
|
||||
const handleUpdateRating = (rating: number) => {
|
||||
if (!detailQuery?.data) return;
|
||||
|
||||
updateRatingMutation.mutate({
|
||||
_serverId: detailQuery?.data.serverId,
|
||||
query: {
|
||||
item: [detailQuery.data],
|
||||
rating,
|
||||
},
|
||||
serverId: detailQuery.data.serverId,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -56,11 +58,11 @@ export const AlbumDetailHeader = forwardRef(
|
|||
if (!detailQuery?.data || !detailQuery?.data.userRating) return;
|
||||
|
||||
updateRatingMutation.mutate({
|
||||
_serverId: detailQuery.data.serverId,
|
||||
query: {
|
||||
item: [detailQuery.data],
|
||||
rating: 0,
|
||||
},
|
||||
serverId: detailQuery.data.serverId,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
import {
|
||||
ALBUM_CARD_ROWS,
|
||||
getColumnDefs,
|
||||
TablePagination,
|
||||
VirtualGridAutoSizerContainer,
|
||||
VirtualInfiniteGrid,
|
||||
VirtualInfiniteGridRef,
|
||||
VirtualTable,
|
||||
} from '/@/renderer/components';
|
||||
import { ALBUM_CARD_ROWS } from '/@/renderer/components';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { ListDisplayType, CardRow } from '/@/renderer/types';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
@ -40,6 +32,12 @@ import { generatePath, useNavigate } from 'react-router';
|
|||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
|
||||
import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context';
|
||||
import {
|
||||
VirtualInfiniteGridRef,
|
||||
VirtualGridAutoSizerContainer,
|
||||
VirtualInfiniteGrid,
|
||||
} from '/@/renderer/components/virtual-grid';
|
||||
import { getColumnDefs, VirtualTable, TablePagination } from '/@/renderer/components/virtual-table';
|
||||
|
||||
interface AlbumListContentProps {
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
|
@ -71,29 +69,36 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
|
|||
limit,
|
||||
startIndex,
|
||||
...filter,
|
||||
jfParams: {
|
||||
...filter.jfParams,
|
||||
},
|
||||
ndParams: {
|
||||
...filter.ndParams,
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
...filter._custom?.jellyfin,
|
||||
},
|
||||
navidrome: {
|
||||
...filter._custom?.navidrome,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const queryKey = queryKeys.albums.list(server?.id || '', query);
|
||||
|
||||
if (!server) {
|
||||
return params.failCallback();
|
||||
}
|
||||
|
||||
const albumsRes = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
const albums = api.normalize.albumList(albumsRes, server);
|
||||
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || 0);
|
||||
return params.successCallback(albumsRes?.items || [], albumsRes?.totalRecordCount || 0);
|
||||
},
|
||||
rowCount: undefined,
|
||||
};
|
||||
|
@ -165,15 +170,21 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
|
|||
|
||||
const fetch = useCallback(
|
||||
async ({ skip, take }: { skip: number; take: number }) => {
|
||||
if (!server) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const query: AlbumListQuery = {
|
||||
limit: take,
|
||||
startIndex: skip,
|
||||
...filter,
|
||||
jfParams: {
|
||||
...filter.jfParams,
|
||||
},
|
||||
ndParams: {
|
||||
...filter.ndParams,
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
...filter._custom?.jellyfin,
|
||||
},
|
||||
navidrome: {
|
||||
...filter._custom?.navidrome,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -181,13 +192,15 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
|
|||
|
||||
const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) =>
|
||||
controller.getAlbumList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
);
|
||||
|
||||
return api.normalize.albumList(albums, server);
|
||||
return albums;
|
||||
},
|
||||
[filter, queryClient, server],
|
||||
);
|
||||
|
@ -268,8 +281,8 @@ export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListCont
|
|||
navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id }));
|
||||
};
|
||||
|
||||
const createFavoriteMutation = useCreateFavorite();
|
||||
const deleteFavoriteMutation = useDeleteFavorite();
|
||||
const createFavoriteMutation = useCreateFavorite({});
|
||||
const deleteFavoriteMutation = useDeleteFavorite({});
|
||||
|
||||
const handleFavorite = (options: {
|
||||
id: string[];
|
||||
|
|
|
@ -20,16 +20,7 @@ import {
|
|||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { AlbumListQuery, AlbumListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
||||
import {
|
||||
ALBUM_TABLE_COLUMNS,
|
||||
Button,
|
||||
DropdownMenu,
|
||||
MultiSelect,
|
||||
Slider,
|
||||
Switch,
|
||||
Text,
|
||||
VirtualInfiniteGridRef,
|
||||
} from '/@/renderer/components';
|
||||
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import {
|
||||
AlbumListFilter,
|
||||
|
@ -43,6 +34,8 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
|
|||
import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters';
|
||||
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
|
||||
import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||
|
||||
const FILTERS = {
|
||||
jellyfin: [
|
||||
|
@ -100,7 +93,7 @@ export const AlbumListHeaderFilters = ({
|
|||
const { display, filter, table, grid } = useAlbumListStore({ id, key: pageKey });
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const musicFoldersQuery = useMusicFolders();
|
||||
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id });
|
||||
|
||||
const sortByLabel =
|
||||
(server?.type &&
|
||||
|
@ -115,13 +108,15 @@ export const AlbumListHeaderFilters = ({
|
|||
limit: take,
|
||||
startIndex: skip,
|
||||
...filters,
|
||||
jfParams: {
|
||||
...filters.jfParams,
|
||||
...customFilters?.jfParams,
|
||||
},
|
||||
ndParams: {
|
||||
...filters.ndParams,
|
||||
...customFilters?.ndParams,
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
...filters._custom?.jellyfin,
|
||||
...customFilters?._custom?.jellyfin,
|
||||
},
|
||||
navidrome: {
|
||||
...filters._custom?.navidrome,
|
||||
...customFilters?._custom?.navidrome,
|
||||
},
|
||||
},
|
||||
...customFilters,
|
||||
};
|
||||
|
@ -132,14 +127,16 @@ export const AlbumListHeaderFilters = ({
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
return api.normalize.albumList(albums, server);
|
||||
return albums;
|
||||
},
|
||||
[customFilters, queryClient, server],
|
||||
);
|
||||
|
@ -157,13 +154,15 @@ export const AlbumListHeaderFilters = ({
|
|||
startIndex,
|
||||
...filters,
|
||||
...customFilters,
|
||||
jfParams: {
|
||||
...filters.jfParams,
|
||||
...customFilters?.jfParams,
|
||||
},
|
||||
ndParams: {
|
||||
...filters.ndParams,
|
||||
...customFilters?.ndParams,
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
...filters._custom?.jellyfin,
|
||||
...customFilters?._custom?.jellyfin,
|
||||
},
|
||||
navidrome: {
|
||||
...filters._custom?.navidrome,
|
||||
...customFilters?._custom?.navidrome,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -173,15 +172,16 @@ export const AlbumListHeaderFilters = ({
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
const albums = api.normalize.albumList(albumsRes, server);
|
||||
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || 0);
|
||||
return params.successCallback(albumsRes?.items || [], albumsRes?.totalRecordCount || 0);
|
||||
},
|
||||
rowCount: undefined,
|
||||
};
|
||||
|
@ -218,6 +218,7 @@ export const AlbumListHeaderFilters = ({
|
|||
handleFilterChange={handleFilterChange}
|
||||
id={id}
|
||||
pageKey={pageKey}
|
||||
serverId={server?.id}
|
||||
/>
|
||||
) : (
|
||||
<JellyfinAlbumFilters
|
||||
|
@ -225,6 +226,7 @@ export const AlbumListHeaderFilters = ({
|
|||
handleFilterChange={handleFilterChange}
|
||||
id={id}
|
||||
pageKey={pageKey}
|
||||
serverId={server?.id}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -293,30 +295,32 @@ export const AlbumListHeaderFilters = ({
|
|||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
|
||||
const handlePlay = async (play: Play) => {
|
||||
if (!itemCount || itemCount === 0) return;
|
||||
if (!itemCount || itemCount === 0 || !server) return;
|
||||
|
||||
const query = {
|
||||
startIndex: 0,
|
||||
...filter,
|
||||
...customFilters,
|
||||
jfParams: {
|
||||
...filter.jfParams,
|
||||
...customFilters?.jfParams,
|
||||
},
|
||||
ndParams: {
|
||||
...filter.ndParams,
|
||||
...customFilters?.ndParams,
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
...filter._custom?.jellyfin,
|
||||
...customFilters?._custom?.jellyfin,
|
||||
},
|
||||
navidrome: {
|
||||
...filter._custom?.navidrome,
|
||||
...customFilters?._custom?.navidrome,
|
||||
},
|
||||
},
|
||||
};
|
||||
const queryKey = queryKeys.albums.list(server?.id || '', query);
|
||||
|
||||
const albumListRes = await queryClient.fetchQuery({
|
||||
queryFn: ({ signal }) => api.controller.getAlbumList({ query, server, signal }),
|
||||
queryFn: ({ signal }) =>
|
||||
api.controller.getAlbumList({ apiClientProps: { server, signal }, query }),
|
||||
queryKey,
|
||||
});
|
||||
|
||||
const albumIds =
|
||||
api.normalize.albumList(albumListRes, server).items?.map((item) => item.id) || [];
|
||||
const albumIds = albumListRes?.items?.map((a) => a.id) || [];
|
||||
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: {
|
||||
|
@ -382,16 +386,16 @@ export const AlbumListHeaderFilters = ({
|
|||
const isFilterApplied = useMemo(() => {
|
||||
const isNavidromeFilterApplied =
|
||||
server?.type === ServerType.NAVIDROME &&
|
||||
filter.ndParams &&
|
||||
Object.values(filter.ndParams).some((value) => value !== undefined);
|
||||
filter?._custom.navidrome &&
|
||||
Object.values(filter._custom.navidrome).some((value) => value !== undefined);
|
||||
|
||||
const isJellyfinFilterApplied =
|
||||
server?.type === ServerType.JELLYFIN &&
|
||||
filter.jfParams &&
|
||||
Object.values(filter.jfParams).some((value) => value !== undefined);
|
||||
filter?._custom.jellyfin &&
|
||||
Object.values(filter._custom.jellyfin).some((value) => value !== undefined);
|
||||
|
||||
return isNavidromeFilterApplied || isJellyfinFilterApplied;
|
||||
}, [filter.jfParams, filter.ndParams, server?.type]);
|
||||
}, [filter._custom.jellyfin, filter._custom.navidrome, server?.type]);
|
||||
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
|
@ -456,7 +460,7 @@ export const AlbumListHeaderFilters = ({
|
|||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{musicFoldersQuery.data?.map((folder) => (
|
||||
{musicFoldersQuery.data?.items.map((folder) => (
|
||||
<DropdownMenu.Item
|
||||
key={`musicFolder-${folder.id}`}
|
||||
$isActive={filter.musicFolderId === folder.id}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { api } from '/@/renderer/api';
|
|||
import { controller } from '/@/renderer/api/controller';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
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 { useContainerQuery } from '/@/renderer/hooks';
|
||||
import {
|
||||
|
@ -24,6 +24,7 @@ import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/a
|
|||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
|
||||
interface AlbumListHeaderProps {
|
||||
customFilters?: Partial<AlbumListFilter>;
|
||||
|
@ -54,15 +55,17 @@ export const AlbumListHeader = ({
|
|||
limit: take,
|
||||
startIndex: skip,
|
||||
...filters,
|
||||
jfParams: {
|
||||
...filters.jfParams,
|
||||
...customFilters?.jfParams,
|
||||
},
|
||||
ndParams: {
|
||||
...filters.ndParams,
|
||||
...customFilters?.ndParams,
|
||||
},
|
||||
...customFilters,
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
...filters._custom?.jellyfin,
|
||||
...customFilters?._custom?.jellyfin,
|
||||
},
|
||||
navidrome: {
|
||||
...filters._custom?.navidrome,
|
||||
...customFilters?._custom?.navidrome,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const queryKey = queryKeys.albums.list(server?.id || '', query);
|
||||
|
@ -71,14 +74,16 @@ export const AlbumListHeader = ({
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
controller.getAlbumList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
return api.normalize.albumList(albums, server);
|
||||
return albums;
|
||||
},
|
||||
[customFilters, queryClient, server],
|
||||
);
|
||||
|
@ -96,13 +101,15 @@ export const AlbumListHeader = ({
|
|||
startIndex,
|
||||
...filters,
|
||||
...customFilters,
|
||||
jfParams: {
|
||||
...filters.jfParams,
|
||||
...customFilters?.jfParams,
|
||||
},
|
||||
ndParams: {
|
||||
...filters.ndParams,
|
||||
...customFilters?.ndParams,
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
...filters._custom?.jellyfin,
|
||||
...customFilters?._custom?.jellyfin,
|
||||
},
|
||||
navidrome: {
|
||||
...filters._custom?.navidrome,
|
||||
...customFilters?._custom?.navidrome,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -112,15 +119,16 @@ export const AlbumListHeader = ({
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
const albums = api.normalize.albumList(albumsRes, server);
|
||||
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || 0);
|
||||
params.successCallback(albumsRes?.items || [], albumsRes?.totalRecordCount || 0);
|
||||
},
|
||||
rowCount: undefined,
|
||||
};
|
||||
|
@ -164,24 +172,26 @@ export const AlbumListHeader = ({
|
|||
startIndex: 0,
|
||||
...filter,
|
||||
...customFilters,
|
||||
jfParams: {
|
||||
...filter.jfParams,
|
||||
...customFilters?.jfParams,
|
||||
},
|
||||
ndParams: {
|
||||
...filter.ndParams,
|
||||
...customFilters?.ndParams,
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
...filter._custom?.jellyfin,
|
||||
...customFilters?._custom?.jellyfin,
|
||||
},
|
||||
navidrome: {
|
||||
...filter._custom?.navidrome,
|
||||
...customFilters?._custom?.navidrome,
|
||||
},
|
||||
},
|
||||
};
|
||||
const queryKey = queryKeys.albums.list(server?.id || '', query);
|
||||
|
||||
const albumListRes = await queryClient.fetchQuery({
|
||||
queryFn: ({ signal }) => api.controller.getAlbumList({ query, server, signal }),
|
||||
queryFn: ({ signal }) =>
|
||||
api.controller.getAlbumList({ apiClientProps: { server, signal }, query }),
|
||||
queryKey,
|
||||
});
|
||||
|
||||
const albumIds =
|
||||
api.normalize.albumList(albumListRes, server).items?.map((item) => item.id) || [];
|
||||
const albumIds = albumListRes?.items?.map((item) => item.id) || [];
|
||||
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: {
|
||||
|
|
|
@ -12,6 +12,7 @@ interface JellyfinAlbumFiltersProps {
|
|||
handleFilterChange: (filters: AlbumListFilter) => void;
|
||||
id?: string;
|
||||
pageKey: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const JellyfinAlbumFilters = ({
|
||||
|
@ -19,24 +20,25 @@ export const JellyfinAlbumFilters = ({
|
|||
handleFilterChange,
|
||||
pageKey,
|
||||
id,
|
||||
serverId,
|
||||
}: JellyfinAlbumFiltersProps) => {
|
||||
const filter = useAlbumListFilter({ id, key: pageKey });
|
||||
const { setFilter } = useListStoreActions();
|
||||
|
||||
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
|
||||
const genreListQuery = useGenreList(null);
|
||||
const genreListQuery = useGenreList({ query: null, serverId });
|
||||
|
||||
const genreList = useMemo(() => {
|
||||
if (!genreListQuery?.data) return [];
|
||||
return genreListQuery.data.map((genre) => ({
|
||||
return genreListQuery.data.items.map((genre) => ({
|
||||
label: genre.name,
|
||||
value: genre.id,
|
||||
}));
|
||||
}, [genreListQuery.data]);
|
||||
|
||||
const selectedGenres = useMemo(() => {
|
||||
return filter.jfParams?.genreIds?.split(',');
|
||||
}, [filter.jfParams?.genreIds]);
|
||||
return filter._custom?.jellyfin?.genreIds?.split(',');
|
||||
}, [filter._custom?.jellyfin?.genreIds]);
|
||||
|
||||
const toggleFilters = [
|
||||
{
|
||||
|
@ -44,17 +46,19 @@ export const JellyfinAlbumFilters = ({
|
|||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
jfParams: {
|
||||
...filter.jfParams,
|
||||
includeItemTypes: 'Audio',
|
||||
isFavorite: e.currentTarget.checked ? true : undefined,
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
jellyfin: {
|
||||
...filter._custom?.jellyfin,
|
||||
isFavorite: e.currentTarget.checked ? true : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
}) as AlbumListFilter;
|
||||
handleFilterChange(updatedFilters);
|
||||
},
|
||||
value: filter.jfParams?.isFavorite,
|
||||
value: filter._custom?.jellyfin?.isFavorite,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -62,9 +66,12 @@ export const JellyfinAlbumFilters = ({
|
|||
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
jfParams: {
|
||||
...filter.jfParams,
|
||||
minYear: e === '' ? undefined : (e as number),
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
jellyfin: {
|
||||
...filter._custom?.jellyfin,
|
||||
minYear: e === '' ? undefined : (e as number),
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
|
@ -76,9 +83,12 @@ export const JellyfinAlbumFilters = ({
|
|||
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
jfParams: {
|
||||
...filter.jfParams,
|
||||
maxYear: e === '' ? undefined : (e as number),
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
jellyfin: {
|
||||
...filter._custom?.jellyfin,
|
||||
maxYear: e === '' ? undefined : (e as number),
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
|
@ -90,9 +100,12 @@ export const JellyfinAlbumFilters = ({
|
|||
const genreFilterString = e?.length ? e.join(',') : undefined;
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
jfParams: {
|
||||
...filter.jfParams,
|
||||
genreIds: genreFilterString,
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
jellyfin: {
|
||||
...filter._custom?.jellyfin,
|
||||
genreIds: genreFilterString,
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
|
@ -102,17 +115,18 @@ export const JellyfinAlbumFilters = ({
|
|||
|
||||
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
|
||||
|
||||
const albumArtistListQuery = useAlbumArtistList(
|
||||
{
|
||||
const albumArtistListQuery = useAlbumArtistList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
},
|
||||
query: {
|
||||
sortBy: AlbumArtistListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
},
|
||||
{
|
||||
cacheTime: 1000 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
},
|
||||
);
|
||||
serverId,
|
||||
});
|
||||
|
||||
const selectableAlbumArtists = useMemo(() => {
|
||||
if (!albumArtistListQuery?.data?.items) return [];
|
||||
|
@ -127,9 +141,12 @@ export const JellyfinAlbumFilters = ({
|
|||
const albumArtistFilterString = e?.length ? e.join(',') : undefined;
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
jfParams: {
|
||||
...filter.jfParams,
|
||||
albumArtistIds: albumArtistFilterString,
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
jellyfin: {
|
||||
...filter._custom?.jellyfin,
|
||||
albumArtistIds: albumArtistFilterString,
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
|
@ -155,21 +172,21 @@ export const JellyfinAlbumFilters = ({
|
|||
<Divider my="0.5rem" />
|
||||
<Group grow>
|
||||
<NumberInput
|
||||
defaultValue={filter.jfParams?.minYear}
|
||||
defaultValue={filter._custom?.jellyfin?.minYear}
|
||||
hideControls={false}
|
||||
label="From year"
|
||||
max={2300}
|
||||
min={1700}
|
||||
required={!!filter.jfParams?.maxYear}
|
||||
required={!!filter._custom?.jellyfin?.maxYear}
|
||||
onChange={(e) => handleMinYearFilter(e)}
|
||||
/>
|
||||
<NumberInput
|
||||
defaultValue={filter.jfParams?.maxYear}
|
||||
defaultValue={filter._custom?.jellyfin?.maxYear}
|
||||
hideControls={false}
|
||||
label="To year"
|
||||
max={2300}
|
||||
min={1700}
|
||||
required={!!filter.jfParams?.minYear}
|
||||
required={!!filter._custom?.jellyfin?.minYear}
|
||||
onChange={(e) => handleMaxYearFilter(e)}
|
||||
/>
|
||||
</Group>
|
||||
|
@ -189,7 +206,7 @@ export const JellyfinAlbumFilters = ({
|
|||
clearable
|
||||
searchable
|
||||
data={selectableAlbumArtists}
|
||||
defaultValue={filter.jfParams?.albumArtistIds?.split(',')}
|
||||
defaultValue={filter._custom?.jellyfin?.albumArtistIds?.split(',')}
|
||||
disabled={disableArtistFilter}
|
||||
label="Artist"
|
||||
limit={300}
|
||||
|
|
|
@ -12,6 +12,7 @@ interface NavidromeAlbumFiltersProps {
|
|||
handleFilterChange: (filters: AlbumListFilter) => void;
|
||||
id?: string;
|
||||
pageKey: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const NavidromeAlbumFilters = ({
|
||||
|
@ -19,15 +20,16 @@ export const NavidromeAlbumFilters = ({
|
|||
disableArtistFilter,
|
||||
pageKey,
|
||||
id,
|
||||
serverId,
|
||||
}: NavidromeAlbumFiltersProps) => {
|
||||
const filter = useAlbumListFilter({ id, key: pageKey });
|
||||
const { setFilter } = useListStoreActions();
|
||||
|
||||
const genreListQuery = useGenreList(null);
|
||||
const genreListQuery = useGenreList({ query: null, serverId });
|
||||
|
||||
const genreList = useMemo(() => {
|
||||
if (!genreListQuery?.data) return [];
|
||||
return genreListQuery.data.map((genre) => ({
|
||||
return genreListQuery.data.items.map((genre) => ({
|
||||
label: genre.name,
|
||||
value: genre.id,
|
||||
}));
|
||||
|
@ -36,9 +38,12 @@ export const NavidromeAlbumFilters = ({
|
|||
const handleGenresFilter = debounce((e: string | null) => {
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
ndParams: {
|
||||
...filter.ndParams,
|
||||
genre_id: e || undefined,
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
navidrome: {
|
||||
...filter._custom?.navidrome,
|
||||
genre_id: e || undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
key: 'album',
|
||||
|
@ -52,70 +57,89 @@ export const NavidromeAlbumFilters = ({
|
|||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
ndParams: {
|
||||
...filter.ndParams,
|
||||
has_rating: e.currentTarget.checked ? true : undefined,
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
navidrome: {
|
||||
...filter._custom?.navidrome,
|
||||
has_rating: e.currentTarget.checked ? true : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
}) as AlbumListFilter;
|
||||
handleFilterChange(updatedFilters);
|
||||
},
|
||||
value: filter.ndParams?.has_rating,
|
||||
value: filter._custom?.navidrome?.has_rating,
|
||||
},
|
||||
{
|
||||
label: 'Is favorited',
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||
console.log('e.currentTarget.checked :>> ', e.currentTarget.checked);
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
ndParams: { ...filter.ndParams, starred: e.currentTarget.checked ? true : undefined },
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
navidrome: {
|
||||
...filter._custom?.navidrome,
|
||||
starred: e.currentTarget.checked ? true : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
}) as AlbumListFilter;
|
||||
handleFilterChange(updatedFilters);
|
||||
},
|
||||
value: filter.ndParams?.starred,
|
||||
value: filter._custom?.navidrome?.starred,
|
||||
},
|
||||
{
|
||||
label: 'Is compilation',
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
ndParams: {
|
||||
...filter.ndParams,
|
||||
compilation: e.currentTarget.checked ? true : undefined,
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
navidrome: {
|
||||
...filter._custom?.navidrome,
|
||||
compilation: e.currentTarget.checked ? true : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
}) as AlbumListFilter;
|
||||
handleFilterChange(updatedFilters);
|
||||
},
|
||||
value: filter.ndParams?.compilation,
|
||||
value: filter._custom?.navidrome?.compilation,
|
||||
},
|
||||
{
|
||||
label: 'Is recently played',
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
ndParams: {
|
||||
...filter.ndParams,
|
||||
recently_played: e.currentTarget.checked ? true : undefined,
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
navidrome: {
|
||||
...filter._custom?.navidrome,
|
||||
recently_played: e.currentTarget.checked ? true : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
}) as AlbumListFilter;
|
||||
handleFilterChange(updatedFilters);
|
||||
},
|
||||
value: filter.ndParams?.recently_played,
|
||||
value: filter._custom?.navidrome?.recently_played,
|
||||
},
|
||||
];
|
||||
|
||||
const handleYearFilter = debounce((e: number | string) => {
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
ndParams: {
|
||||
...filter.ndParams,
|
||||
year: e === '' ? undefined : (e as number),
|
||||
_custom: {
|
||||
navidrome: {
|
||||
...filter._custom?.navidrome,
|
||||
year: e === '' ? undefined : (e as number),
|
||||
},
|
||||
...filter._custom,
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
|
@ -125,18 +149,19 @@ export const NavidromeAlbumFilters = ({
|
|||
|
||||
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
|
||||
|
||||
const albumArtistListQuery = useAlbumArtistList(
|
||||
{
|
||||
const albumArtistListQuery = useAlbumArtistList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
},
|
||||
query: {
|
||||
// searchTerm: debouncedSearchTerm,
|
||||
sortBy: AlbumArtistListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
},
|
||||
{
|
||||
cacheTime: 1000 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 1,
|
||||
},
|
||||
);
|
||||
serverId,
|
||||
});
|
||||
|
||||
const selectableAlbumArtists = useMemo(() => {
|
||||
if (!albumArtistListQuery?.data?.items) return [];
|
||||
|
@ -150,9 +175,12 @@ export const NavidromeAlbumFilters = ({
|
|||
const handleAlbumArtistFilter = (e: string | null) => {
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
ndParams: {
|
||||
...filter.ndParams,
|
||||
artist_id: e || undefined,
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
navidrome: {
|
||||
...filter._custom?.navidrome,
|
||||
artist_id: e || undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
|
@ -177,7 +205,7 @@ export const NavidromeAlbumFilters = ({
|
|||
<Divider my="0.5rem" />
|
||||
<Group grow>
|
||||
<NumberInput
|
||||
defaultValue={filter.ndParams?.year}
|
||||
defaultValue={filter._custom?.navidrome?.year}
|
||||
hideControls={false}
|
||||
label="Year"
|
||||
max={5000}
|
||||
|
@ -188,7 +216,7 @@ export const NavidromeAlbumFilters = ({
|
|||
clearable
|
||||
searchable
|
||||
data={genreList}
|
||||
defaultValue={filter.ndParams?.genre_id}
|
||||
defaultValue={filter._custom?.navidrome?.genre_id}
|
||||
label="Genre"
|
||||
onChange={handleGenresFilter}
|
||||
/>
|
||||
|
@ -198,7 +226,7 @@ export const NavidromeAlbumFilters = ({
|
|||
clearable
|
||||
searchable
|
||||
data={selectableAlbumArtists}
|
||||
defaultValue={filter.ndParams?.artist_id}
|
||||
defaultValue={filter._custom?.navidrome?.artist_id}
|
||||
disabled={disableArtistFilter}
|
||||
label="Artist"
|
||||
limit={300}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { AlbumDetailHeader } from '/@/renderer/features/albums/components/album-
|
|||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
|
||||
const AlbumDetailRoute = () => {
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
|
@ -17,7 +18,8 @@ const AlbumDetailRoute = () => {
|
|||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { albumId } = useParams() as { albumId: string };
|
||||
const detailQuery = useAlbumDetail({ id: albumId });
|
||||
const server = useCurrentServer();
|
||||
const detailQuery = useAlbumDetail({ query: { id: albumId }, serverId: server?.id });
|
||||
const background = useFastAverageColor(detailQuery.data?.imageUrl, !detailQuery.isLoading);
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { VirtualInfiniteGridRef } from '/@/renderer/components';
|
||||
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
|
||||
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
|
||||
|
@ -8,6 +7,7 @@ import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-que
|
|||
import { generatePageKey, useAlbumListFilter, useCurrentServer } from '/@/renderer/store';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { AlbumListContext } from '/@/renderer/features/albums/context/album-list-context';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
|
||||
const AlbumListRoute = () => {
|
||||
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||
|
@ -24,17 +24,18 @@ const AlbumListRoute = () => {
|
|||
|
||||
const albumListFilter = useAlbumListFilter({ id: albumArtistId || undefined, key: pageKey });
|
||||
|
||||
const itemCountCheck = useAlbumList(
|
||||
{
|
||||
const itemCountCheck = useAlbumList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
query: {
|
||||
limit: 1,
|
||||
startIndex: 0,
|
||||
...albumListFilter,
|
||||
},
|
||||
{
|
||||
cacheTime: 1000 * 60,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
);
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const itemCount =
|
||||
itemCountCheck.data?.totalRecordCount === null
|
||||
|
|
|
@ -1,12 +1,5 @@
|
|||
import { useMemo } from 'react';
|
||||
import {
|
||||
Button,
|
||||
getColumnDefs,
|
||||
GridCarousel,
|
||||
Text,
|
||||
TextTitle,
|
||||
VirtualTable,
|
||||
} from '/@/renderer/components';
|
||||
import { Button, GridCarousel, Text, TextTitle } from '/@/renderer/components';
|
||||
import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
|
||||
import { Box, Group, Stack } from '@mantine/core';
|
||||
import { RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
|
||||
|
@ -38,6 +31,7 @@ import {
|
|||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query';
|
||||
import { useTopSongsList } from '/@/renderer/features/artists/queries/top-songs-list-query';
|
||||
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
position: relative;
|
||||
|
@ -63,7 +57,7 @@ export const AlbumArtistDetailContent = () => {
|
|||
const server = useCurrentServer();
|
||||
const itemsPerPage = cq.isXl ? 9 : cq.isLg ? 7 : cq.isMd ? 5 : cq.isSm ? 4 : 3;
|
||||
|
||||
const detailQuery = useAlbumArtistDetail({ id: albumArtistId });
|
||||
const detailQuery = useAlbumArtistDetail({ query: { id: albumArtistId }, serverId: server?.id });
|
||||
|
||||
const artistDiscographyLink = `${generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL_DISCOGRAPHY, {
|
||||
albumArtistId,
|
||||
|
@ -80,34 +74,57 @@ export const AlbumArtistDetailContent = () => {
|
|||
})}`;
|
||||
|
||||
const recentAlbumsQuery = useAlbumList({
|
||||
jfParams: server?.type === ServerType.JELLYFIN ? { artistIds: albumArtistId } : undefined,
|
||||
limit: itemsPerPage,
|
||||
ndParams:
|
||||
server?.type === ServerType.NAVIDROME
|
||||
? { artist_id: albumArtistId, compilation: false }
|
||||
: undefined,
|
||||
sortBy: AlbumListSort.RELEASE_DATE,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
query: {
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
...(server?.type === ServerType.JELLYFIN ? { artistIds: albumArtistId } : undefined),
|
||||
},
|
||||
navidrome: {
|
||||
...(server?.type === ServerType.NAVIDROME
|
||||
? { artist_id: albumArtistId, compilation: false }
|
||||
: undefined),
|
||||
},
|
||||
},
|
||||
limit: itemsPerPage,
|
||||
sortBy: AlbumListSort.RELEASE_DATE,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const compilationAlbumsQuery = useAlbumList({
|
||||
jfParams:
|
||||
server?.type === ServerType.JELLYFIN ? { contributingArtistIds: albumArtistId } : undefined,
|
||||
limit: itemsPerPage,
|
||||
ndParams:
|
||||
server?.type === ServerType.NAVIDROME
|
||||
? { artist_id: albumArtistId, compilation: true }
|
||||
: undefined,
|
||||
sortBy: AlbumListSort.RELEASE_DATE,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
query: {
|
||||
_custom: {
|
||||
jellyfin: {
|
||||
...(server?.type === ServerType.JELLYFIN
|
||||
? { contributingArtistIds: albumArtistId }
|
||||
: undefined),
|
||||
},
|
||||
navidrome: {
|
||||
...(server?.type === ServerType.NAVIDROME
|
||||
? { artist_id: albumArtistId, compilation: true }
|
||||
: undefined),
|
||||
},
|
||||
},
|
||||
limit: itemsPerPage,
|
||||
sortBy: AlbumListSort.RELEASE_DATE,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const topSongsQuery = useTopSongsList(
|
||||
{ artist: detailQuery?.data?.name || '', artistId: albumArtistId },
|
||||
{ enabled: !!detailQuery?.data?.name },
|
||||
);
|
||||
const topSongsQuery = useTopSongsList({
|
||||
options: {
|
||||
enabled: !!detailQuery?.data?.name,
|
||||
},
|
||||
query: {
|
||||
artist: detailQuery?.data?.name || '',
|
||||
artistId: albumArtistId,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const topSongsColumnDefs: ColDef[] = useMemo(
|
||||
() =>
|
||||
|
@ -242,8 +259,8 @@ export const AlbumArtistDetailContent = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const createFavoriteMutation = useCreateFavorite();
|
||||
const deleteFavoriteMutation = useDeleteFavorite();
|
||||
const createFavoriteMutation = useCreateFavorite({});
|
||||
const deleteFavoriteMutation = useDeleteFavorite({});
|
||||
|
||||
const handleFavorite = () => {
|
||||
if (!detailQuery?.data) return;
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { Group, Rating, Stack } from '@mantine/core';
|
||||
import { forwardRef, Fragment, Ref, MouseEvent } from 'react';
|
||||
import { Group, Rating, Stack } from '@mantine/core';
|
||||
import { useParams } from 'react-router';
|
||||
import { LibraryItem, ServerType } from '/@/renderer/api/types';
|
||||
import { Text } from '/@/renderer/components';
|
||||
import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query';
|
||||
import { LibraryHeader, useUpdateRating } from '/@/renderer/features/shared';
|
||||
import { LibraryHeader, useSetRating } from '/@/renderer/features/shared';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { formatDurationString } from '/@/renderer/utils';
|
||||
import { useCurrentServer } from '../../../store/auth.store';
|
||||
|
||||
interface AlbumArtistDetailHeaderProps {
|
||||
background: string;
|
||||
|
@ -16,7 +17,11 @@ interface AlbumArtistDetailHeaderProps {
|
|||
export const AlbumArtistDetailHeader = forwardRef(
|
||||
({ background }: AlbumArtistDetailHeaderProps, ref: Ref<HTMLDivElement>) => {
|
||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
||||
const detailQuery = useAlbumArtistDetail({ id: albumArtistId });
|
||||
const server = useCurrentServer();
|
||||
const detailQuery = useAlbumArtistDetail({
|
||||
query: { id: albumArtistId },
|
||||
serverId: server?.id,
|
||||
});
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const metadataItems = [
|
||||
|
@ -37,17 +42,17 @@ export const AlbumArtistDetailHeader = forwardRef(
|
|||
},
|
||||
];
|
||||
|
||||
const updateRatingMutation = useUpdateRating();
|
||||
const updateRatingMutation = useSetRating({});
|
||||
|
||||
const handleUpdateRating = (rating: number) => {
|
||||
if (!detailQuery?.data) return;
|
||||
|
||||
updateRatingMutation.mutate({
|
||||
_serverId: detailQuery?.data.serverId,
|
||||
query: {
|
||||
item: [detailQuery.data],
|
||||
rating,
|
||||
},
|
||||
serverId: detailQuery?.data.serverId,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -58,11 +63,11 @@ export const AlbumArtistDetailHeader = forwardRef(
|
|||
if (!isSameRatingAsPrevious) return;
|
||||
|
||||
updateRatingMutation.mutate({
|
||||
_serverId: detailQuery.data.serverId,
|
||||
query: {
|
||||
item: [detailQuery.data],
|
||||
rating: 0,
|
||||
},
|
||||
serverId: detailQuery.data.serverId,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { MutableRefObject, useMemo } from 'react';
|
||||
import type { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { getColumnDefs, VirtualGridAutoSizerContainer, VirtualTable } from '/@/renderer/components';
|
||||
import { useCurrentServer, useSongListStore } from '/@/renderer/store';
|
||||
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
|
||||
import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { LibraryItem, QueueSong } from '/@/renderer/api/types';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
||||
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
|
||||
|
||||
interface AlbumArtistSongListContentProps {
|
||||
data: QueueSong[];
|
||||
|
|
|
@ -1,12 +1,4 @@
|
|||
import {
|
||||
ALBUMARTIST_CARD_ROWS,
|
||||
getColumnDefs,
|
||||
TablePagination,
|
||||
VirtualGridAutoSizerContainer,
|
||||
VirtualInfiniteGrid,
|
||||
VirtualInfiniteGridRef,
|
||||
VirtualTable,
|
||||
} from '/@/renderer/components';
|
||||
import { ALBUMARTIST_CARD_ROWS } from '/@/renderer/components';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { ListDisplayType, CardRow } from '/@/renderer/types';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
@ -35,6 +27,12 @@ import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-a
|
|||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { useAlbumArtistListFilter, useListStoreActions } from '../../../store/list.store';
|
||||
import { useAlbumArtistListContext } from '/@/renderer/features/artists/context/album-artist-list-context';
|
||||
import {
|
||||
VirtualInfiniteGridRef,
|
||||
VirtualGridAutoSizerContainer,
|
||||
VirtualInfiniteGrid,
|
||||
} from '/@/renderer/components/virtual-grid';
|
||||
import { getColumnDefs, VirtualTable, TablePagination } from '/@/renderer/components/virtual-table';
|
||||
|
||||
interface AlbumArtistListContentProps {
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
|
@ -54,17 +52,18 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon
|
|||
|
||||
const isPaginationEnabled = display === ListDisplayType.TABLE_PAGINATED;
|
||||
|
||||
const checkAlbumArtistList = useAlbumArtistList(
|
||||
{
|
||||
const checkAlbumArtistList = useAlbumArtistList({
|
||||
options: {
|
||||
cacheTime: Infinity,
|
||||
staleTime: 60 * 1000 * 5,
|
||||
},
|
||||
query: {
|
||||
limit: 1,
|
||||
startIndex: 0,
|
||||
...filter,
|
||||
},
|
||||
{
|
||||
cacheTime: Infinity,
|
||||
staleTime: 60 * 1000 * 5,
|
||||
},
|
||||
);
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const columnDefs: ColDef[] = useMemo(() => getColumnDefs(table.columns), [table.columns]);
|
||||
|
||||
|
@ -85,19 +84,23 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filter,
|
||||
},
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
const albums = api.normalize.albumArtistList(albumArtistsRes, server);
|
||||
params.successCallback(albums?.items || [], albumArtistsRes?.totalRecordCount || 0);
|
||||
params.successCallback(
|
||||
albumArtistsRes?.items || [],
|
||||
albumArtistsRes?.totalRecordCount || 0,
|
||||
);
|
||||
},
|
||||
rowCount: undefined,
|
||||
};
|
||||
|
@ -181,18 +184,20 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filter,
|
||||
},
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
return api.normalize.albumArtistList(albumArtistsRes, server);
|
||||
return albumArtistsRes;
|
||||
},
|
||||
[filter, queryClient, server],
|
||||
);
|
||||
|
@ -259,27 +264,29 @@ export const AlbumArtistListContent = ({ gridRef, tableRef }: AlbumArtistListCon
|
|||
{display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<VirtualInfiniteGrid
|
||||
ref={gridRef}
|
||||
cardRows={cardRows}
|
||||
display={display || ListDisplayType.CARD}
|
||||
fetchFn={fetch}
|
||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||
height={height}
|
||||
initialScrollOffset={grid?.scrollOffset || 0}
|
||||
itemCount={checkAlbumArtistList?.data?.totalRecordCount || 0}
|
||||
itemGap={20}
|
||||
itemSize={grid?.itemsPerRow || 5}
|
||||
itemType={LibraryItem.ALBUM_ARTIST}
|
||||
loading={checkAlbumArtistList.isLoading}
|
||||
minimumBatchSize={40}
|
||||
route={{
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
}}
|
||||
width={width}
|
||||
onScroll={handleGridScroll}
|
||||
/>
|
||||
<>
|
||||
<VirtualInfiniteGrid
|
||||
ref={gridRef}
|
||||
cardRows={cardRows}
|
||||
display={display || ListDisplayType.CARD}
|
||||
fetchFn={fetch}
|
||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||
height={height}
|
||||
initialScrollOffset={grid?.scrollOffset || 0}
|
||||
itemCount={checkAlbumArtistList?.data?.totalRecordCount || 0}
|
||||
itemGap={20}
|
||||
itemSize={grid?.itemsPerRow || 5}
|
||||
itemType={LibraryItem.ALBUM_ARTIST}
|
||||
loading={checkAlbumArtistList.isLoading}
|
||||
minimumBatchSize={40}
|
||||
route={{
|
||||
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
|
||||
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
|
||||
}}
|
||||
width={width}
|
||||
onScroll={handleGridScroll}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</AutoSizer>
|
||||
) : (
|
||||
|
|
|
@ -15,16 +15,7 @@ import {
|
|||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { AlbumArtistListSort, SortOrder } from '/@/renderer/api/types';
|
||||
import {
|
||||
DropdownMenu,
|
||||
ALBUMARTIST_TABLE_COLUMNS,
|
||||
VirtualInfiniteGridRef,
|
||||
Text,
|
||||
Button,
|
||||
Slider,
|
||||
MultiSelect,
|
||||
Switch,
|
||||
} from '/@/renderer/components';
|
||||
import { DropdownMenu, Text, Button, Slider, MultiSelect, Switch } from '/@/renderer/components';
|
||||
import { useMusicFolders } from '/@/renderer/features/shared';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import {
|
||||
|
@ -36,6 +27,8 @@ import {
|
|||
} from '/@/renderer/store';
|
||||
import { ListDisplayType, TableColumn, ServerType } from '/@/renderer/types';
|
||||
import { useAlbumArtistListContext } from '../context/album-artist-list-context';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||
|
||||
const FILTERS = {
|
||||
jellyfin: [
|
||||
|
@ -83,7 +76,7 @@ export const AlbumArtistListHeaderFilters = ({
|
|||
const filter = useAlbumArtistListFilter({ key: pageKey });
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const musicFoldersQuery = useMusicFolders();
|
||||
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id });
|
||||
|
||||
const sortByLabel =
|
||||
(server?.type &&
|
||||
|
@ -114,18 +107,20 @@ export const AlbumArtistListHeaderFilters = ({
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
return api.normalize.albumArtistList(albums, server);
|
||||
return albums;
|
||||
},
|
||||
[queryClient, server],
|
||||
);
|
||||
|
@ -148,20 +143,21 @@ export const AlbumArtistListHeaderFilters = ({
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
const albumArtists = api.normalize.albumArtistList(albumArtistsRes, server);
|
||||
params.successCallback(
|
||||
albumArtists?.items || [],
|
||||
albumArtistsRes?.items || [],
|
||||
albumArtistsRes?.totalRecordCount || 0,
|
||||
);
|
||||
},
|
||||
|
@ -355,7 +351,7 @@ export const AlbumArtistListHeaderFilters = ({
|
|||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{musicFoldersQuery.data?.map((folder) => (
|
||||
{musicFoldersQuery.data?.items.map((folder) => (
|
||||
<DropdownMenu.Item
|
||||
key={`musicFolder-${folder.id}`}
|
||||
$isActive={filter.musicFolderId === folder.id}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||
import debounce from 'lodash/debounce';
|
||||
import { api } from '/@/renderer/api';
|
||||
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 { useContainerQuery } from '/@/renderer/hooks';
|
||||
import {
|
||||
|
@ -20,6 +20,7 @@ import { ListDisplayType } from '/@/renderer/types';
|
|||
import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters';
|
||||
import { useAlbumArtistListContext } from '/@/renderer/features/artists/context/album-artist-list-context';
|
||||
import { FilterBar } from '../../shared/components/filter-bar';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
|
||||
interface AlbumArtistListHeaderProps {
|
||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||
|
@ -51,18 +52,20 @@ export const AlbumArtistListHeader = ({
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
return api.normalize.albumArtistList(albums, server);
|
||||
return albums;
|
||||
},
|
||||
[queryClient, server],
|
||||
);
|
||||
|
@ -85,20 +88,21 @@ export const AlbumArtistListHeader = ({
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getAlbumArtistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
const albumArtists = api.normalize.albumArtistList(albumArtistsRes, server);
|
||||
params.successCallback(
|
||||
albumArtists?.items || [],
|
||||
albumArtistsRes?.items || [],
|
||||
albumArtistsRes?.totalRecordCount || 0,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -9,15 +9,17 @@ import { LibraryItem } from '/@/renderer/api/types';
|
|||
import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query';
|
||||
import { AlbumArtistDetailHeader } from '/@/renderer/features/artists/components/album-artist-detail-header';
|
||||
import { AlbumArtistDetailContent } from '/@/renderer/features/artists/components/album-artist-detail-content';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
|
||||
const AlbumArtistDetailRoute = () => {
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const server = useCurrentServer();
|
||||
|
||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
const detailQuery = useAlbumArtistDetail({ id: albumArtistId });
|
||||
const detailQuery = useAlbumArtistDetail({ query: { id: albumArtistId }, serverId: server?.id });
|
||||
const background = useFastAverageColor(detailQuery.data?.imageUrl, !detailQuery.isLoading);
|
||||
|
||||
const handlePlay = () => {
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { useRef } from 'react';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { useParams } from 'react-router';
|
||||
import { AlbumArtistDetailTopSongsListContent } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-content';
|
||||
import { AlbumArtistDetailTopSongsListHeader } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-header';
|
||||
import { useAlbumArtistDetail } from '/@/renderer/features/artists/queries/album-artist-detail-query';
|
||||
import { useTopSongsList } from '/@/renderer/features/artists/queries/top-songs-list-query';
|
||||
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||
import { useCurrentServer } from '../../../store/auth.store';
|
||||
|
||||
const AlbumArtistDetailTopSongsListRoute = () => {
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
const { albumArtistId } = useParams() as { albumArtistId: string };
|
||||
const server = useCurrentServer();
|
||||
|
||||
const detailQuery = useAlbumArtistDetail({ id: albumArtistId });
|
||||
const detailQuery = useAlbumArtistDetail({ query: { id: albumArtistId }, serverId: server?.id });
|
||||
|
||||
const topSongsQuery = useTopSongsList(
|
||||
{ artist: detailQuery?.data?.name || '', artistId: albumArtistId },
|
||||
{ enabled: !!detailQuery?.data?.name },
|
||||
);
|
||||
const topSongsQuery = useTopSongsList({
|
||||
options: { enabled: !!detailQuery?.data?.name },
|
||||
query: { artist: detailQuery?.data?.name || '', artistId: albumArtistId },
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const itemCount = topSongsQuery?.data?.items?.length || 0;
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { VirtualInfiniteGridRef } from '/@/renderer/components';
|
||||
import { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header';
|
||||
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
@ -7,25 +6,29 @@ import { AlbumArtistListContent } from '/@/renderer/features/artists/components/
|
|||
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
|
||||
import { generatePageKey, useAlbumArtistListFilter } from '/@/renderer/store';
|
||||
import { AlbumArtistListContext } from '/@/renderer/features/artists/context/album-artist-list-context';
|
||||
import { useCurrentServer } from '../../../store/auth.store';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
|
||||
const AlbumArtistListRoute = () => {
|
||||
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
const pageKey = generatePageKey('albumArtist', undefined);
|
||||
const server = useCurrentServer();
|
||||
|
||||
const albumArtistListFilter = useAlbumArtistListFilter({ id: undefined, key: pageKey });
|
||||
|
||||
const itemCountCheck = useAlbumArtistList(
|
||||
{
|
||||
const itemCountCheck = useAlbumArtistList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
query: {
|
||||
limit: 1,
|
||||
startIndex: 0,
|
||||
...albumArtistListFilter,
|
||||
},
|
||||
{
|
||||
cacheTime: 1000 * 60,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
);
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const itemCount =
|
||||
itemCountCheck.data?.totalRecordCount === null
|
||||
|
|
|
@ -43,7 +43,7 @@ import {
|
|||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { useDeletePlaylist } from '/@/renderer/features/playlists';
|
||||
import { useRemoveFromPlaylist } from '/@/renderer/features/playlists/mutations/remove-from-playlist-mutation';
|
||||
import { useCreateFavorite, useDeleteFavorite, useUpdateRating } from '/@/renderer/features/shared';
|
||||
import { useCreateFavorite, useDeleteFavorite, useSetRating } from '/@/renderer/features/shared';
|
||||
import { useAuthStore, useCurrentServer, useQueueControls } from '/@/renderer/store';
|
||||
import { usePlayerType } from '/@/renderer/store/settings.store';
|
||||
import { Play, PlaybackType } from '/@/renderer/types';
|
||||
|
@ -190,7 +190,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
[ctx.data, ctx.type, handlePlayQueueAdd],
|
||||
);
|
||||
|
||||
const deletePlaylistMutation = useDeletePlaylist();
|
||||
const deletePlaylistMutation = useDeletePlaylist({});
|
||||
|
||||
const handleDeletePlaylist = useCallback(() => {
|
||||
for (const item of ctx.data) {
|
||||
|
@ -236,8 +236,8 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
});
|
||||
}, [ctx.data, handleDeletePlaylist]);
|
||||
|
||||
const createFavoriteMutation = useCreateFavorite();
|
||||
const deleteFavoriteMutation = useDeleteFavorite();
|
||||
const createFavoriteMutation = useCreateFavorite({});
|
||||
const deleteFavoriteMutation = useDeleteFavorite({});
|
||||
const handleAddToFavorites = useCallback(() => {
|
||||
if (!ctx.dataNodes && !ctx.data) return;
|
||||
|
||||
|
@ -414,7 +414,7 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
serverType,
|
||||
]);
|
||||
|
||||
const updateRatingMutation = useUpdateRating();
|
||||
const updateRatingMutation = useSetRating({});
|
||||
|
||||
const handleUpdateRating = useCallback(
|
||||
(rating: number) => {
|
||||
|
@ -450,11 +450,11 @@ export const ContextMenuProvider = ({ children }: ContextMenuProviderProps) => {
|
|||
|
||||
updateRatingMutation.mutate(
|
||||
{
|
||||
_serverId: serverId,
|
||||
query: {
|
||||
item: items,
|
||||
rating,
|
||||
},
|
||||
serverId,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
|
|
|
@ -23,75 +23,80 @@ const HomeRoute = () => {
|
|||
recentlyPlayed: 0,
|
||||
});
|
||||
|
||||
const feature = useAlbumList(
|
||||
{
|
||||
const feature = useAlbumList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
query: {
|
||||
limit: 20,
|
||||
sortBy: AlbumListSort.RANDOM,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: 0,
|
||||
},
|
||||
{
|
||||
cacheTime: 1000 * 60,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
);
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const featureItemsWithImage = useMemo(() => {
|
||||
return feature.data?.items?.filter((item) => item.imageUrl) ?? [];
|
||||
}, [feature.data?.items]);
|
||||
|
||||
const random = useAlbumList(
|
||||
{
|
||||
const random = useAlbumList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60,
|
||||
keepPreviousData: true,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
query: {
|
||||
limit: itemsPerPage,
|
||||
sortBy: AlbumListSort.RANDOM,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: pagination.random * itemsPerPage,
|
||||
},
|
||||
{
|
||||
cacheTime: 1000 * 60,
|
||||
keepPreviousData: true,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
);
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const recentlyPlayed = useRecentlyPlayed(
|
||||
{
|
||||
const recentlyPlayed = useRecentlyPlayed({
|
||||
options: {
|
||||
keepPreviousData: true,
|
||||
staleTime: 0,
|
||||
},
|
||||
query: {
|
||||
limit: itemsPerPage,
|
||||
sortBy: AlbumListSort.RECENTLY_PLAYED,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: pagination.recentlyPlayed * itemsPerPage,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 0,
|
||||
},
|
||||
);
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const recentlyAdded = useAlbumList(
|
||||
{
|
||||
const recentlyAdded = useAlbumList({
|
||||
options: {
|
||||
keepPreviousData: true,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
query: {
|
||||
limit: itemsPerPage,
|
||||
sortBy: AlbumListSort.RECENTLY_ADDED,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: pagination.recentlyAdded * itemsPerPage,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
);
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const mostPlayed = useAlbumList(
|
||||
{
|
||||
const mostPlayed = useAlbumList({
|
||||
options: {
|
||||
keepPreviousData: true,
|
||||
staleTime: 1000 * 60 * 60,
|
||||
},
|
||||
query: {
|
||||
limit: itemsPerPage,
|
||||
sortBy: AlbumListSort.PLAY_COUNT,
|
||||
sortOrder: SortOrder.DESC,
|
||||
startIndex: pagination.mostPlayed * itemsPerPage,
|
||||
},
|
||||
{
|
||||
keepPreviousData: true,
|
||||
staleTime: 1000 * 60 * 60,
|
||||
},
|
||||
);
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const handleNextPage = useCallback(
|
||||
(key: 'mostPlayed' | 'random' | 'recentlyAdded' | 'recentlyPlayed') => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import type { MutableRefObject } from 'react';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { Group } from '@mantine/core';
|
||||
import { Button, Popover, TableConfigDropdown } from '/@/renderer/components';
|
||||
import { Button, Popover } from '/@/renderer/components';
|
||||
import isElectron from 'is-electron';
|
||||
import {
|
||||
RiArrowDownLine,
|
||||
|
@ -16,6 +16,7 @@ import { usePlayerControls, useQueueControls } from '/@/renderer/store';
|
|||
import { PlaybackType, TableType } from '/@/renderer/types';
|
||||
import { usePlayerType } from '/@/renderer/store/settings.store';
|
||||
import { useSetCurrentTime } from '../../../store/player.store';
|
||||
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import type {
|
|||
} from '@ag-grid-community/core';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import '@ag-grid-community/styles/ag-theme-alpine.css';
|
||||
import { VirtualGridAutoSizerContainer, getColumnDefs } from '/@/renderer/components';
|
||||
import {
|
||||
useAppStoreActions,
|
||||
useCurrentSong,
|
||||
|
@ -27,12 +26,13 @@ import { useMergedRef } from '@mantine/hooks';
|
|||
import isElectron from 'is-electron';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { VirtualTable } from '/@/renderer/components/virtual-table';
|
||||
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
|
||||
import { ErrorFallback } from '/@/renderer/features/action-required';
|
||||
import { PlaybackType, TableType } from '/@/renderer/types';
|
||||
import { LibraryItem, QueueSong } from '/@/renderer/api/types';
|
||||
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
|
||||
import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
const utils = isElectron() ? window.electron.utils : null;
|
||||
|
|
|
@ -4,8 +4,9 @@ import { NowPlayingHeader } from '/@/renderer/features/now-playing/components/no
|
|||
import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue';
|
||||
import type { Song } from '/@/renderer/api/types';
|
||||
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||
import { Paper, VirtualGridContainer } from '/@/renderer/components';
|
||||
import { Paper } from '/@/renderer/components';
|
||||
import { PlayQueueListControls } from '/@/renderer/features/now-playing/components/play-queue-list-controls';
|
||||
import { VirtualGridContainer } from '/@/renderer/components/virtual-grid';
|
||||
|
||||
const NowPlayingRoute = () => {
|
||||
const queueRef = useRef<{ grid: AgGridReactType<Song> } | null>(null);
|
||||
|
|
|
@ -5,7 +5,7 @@ import { Variants, motion } from 'framer-motion';
|
|||
import { RiArrowDownSLine, RiSettings3Line } from 'react-icons/ri';
|
||||
import { useLocation } from 'react-router';
|
||||
import styled from 'styled-components';
|
||||
import { Button, Option, Popover, Switch, TableConfigDropdown } from '/@/renderer/components';
|
||||
import { Button, Option, Popover, Switch } from '/@/renderer/components';
|
||||
import {
|
||||
useCurrentSong,
|
||||
useFullScreenPlayerStore,
|
||||
|
@ -14,6 +14,7 @@ import {
|
|||
import { useFastAverageColor } from '../../../hooks/use-fast-average-color';
|
||||
import { FullScreenPlayerImage } from '/@/renderer/features/player/components/full-screen-player-image';
|
||||
import { FullScreenPlayerQueue } from '/@/renderer/features/player/components/full-screen-player-queue';
|
||||
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
|
||||
|
||||
const Container = styled(motion.div)`
|
||||
z-index: 100;
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
import { useRightControls } from '../hooks/use-right-controls';
|
||||
import { PlayerButton } from './player-button';
|
||||
import { LibraryItem, ServerType } from '/@/renderer/api/types';
|
||||
import { useCreateFavorite, useDeleteFavorite, useUpdateRating } from '/@/renderer/features/shared';
|
||||
import { useCreateFavorite, useDeleteFavorite, useSetRating } from '/@/renderer/features/shared';
|
||||
import { Rating } from '/@/renderer/components';
|
||||
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
|
||||
|
||||
|
@ -32,9 +32,9 @@ export const RightControls = () => {
|
|||
const { rightExpanded: isQueueExpanded } = useSidebarStore();
|
||||
const { handleVolumeSlider, handleVolumeWheel, handleMute } = useRightControls();
|
||||
|
||||
const updateRatingMutation = useUpdateRating();
|
||||
const addToFavoritesMutation = useCreateFavorite();
|
||||
const removeFromFavoritesMutation = useDeleteFavorite();
|
||||
const updateRatingMutation = useSetRating({});
|
||||
const addToFavoritesMutation = useCreateFavorite({});
|
||||
const removeFromFavoritesMutation = useDeleteFavorite({});
|
||||
|
||||
const handleAddToFavorites = () => {
|
||||
if (!currentSong) return;
|
||||
|
@ -51,11 +51,11 @@ export const RightControls = () => {
|
|||
if (!currentSong) return;
|
||||
|
||||
updateRatingMutation.mutate({
|
||||
_serverId: currentSong?.serverId,
|
||||
query: {
|
||||
item: [currentSong],
|
||||
rating,
|
||||
},
|
||||
serverId: currentSong?.serverId,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -63,11 +63,11 @@ export const RightControls = () => {
|
|||
if (!currentSong || !rating) return;
|
||||
|
||||
updateRatingMutation.mutate({
|
||||
_serverId: currentSong?.serverId,
|
||||
query: {
|
||||
item: [currentSong],
|
||||
rating: 0,
|
||||
},
|
||||
serverId: currentSong?.serverId,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -1,20 +1,11 @@
|
|||
import { useCallback } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '/@/renderer/api/index';
|
||||
import { jfNormalize } from '/@/renderer/api/jellyfin.api';
|
||||
import { JFSong } from '/@/renderer/api/jellyfin.types';
|
||||
import { ndNormalize } from '/@/renderer/api/navidrome.api';
|
||||
import { NDSong } from '/@/renderer/api/navidrome.types';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import {
|
||||
useAuthStore,
|
||||
useCurrentServer,
|
||||
usePlayerControls,
|
||||
usePlayerStore,
|
||||
} from '/@/renderer/store';
|
||||
import { useCurrentServer, usePlayerControls, usePlayerStore } from '/@/renderer/store';
|
||||
import { usePlayerType } from '/@/renderer/store/settings.store';
|
||||
import { PlayQueueAddOptions, Play, PlaybackType } from '/@/renderer/types';
|
||||
import { toast } from '/@/renderer/components/toast';
|
||||
import { toast } from '/@/renderer/components/toast/index';
|
||||
import isElectron from 'is-electron';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
import { LibraryItem, SongListSort, SortOrder } from '/@/renderer/api/types';
|
||||
|
@ -25,7 +16,6 @@ const mpris = isElectron() && utils?.isLinux() ? window.electron.mpris : null;
|
|||
export const useHandlePlayQueueAdd = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const playerType = usePlayerType();
|
||||
const deviceId = useAuthStore.getState().deviceId;
|
||||
const server = useCurrentServer();
|
||||
const { play } = usePlayerControls();
|
||||
|
||||
|
@ -114,9 +104,11 @@ export const useHandlePlayQueueAdd = () => {
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getPlaylistSongList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: queryFilter,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{
|
||||
cacheTime: 1000 * 60,
|
||||
|
@ -128,9 +120,11 @@ export const useHandlePlayQueueAdd = () => {
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getSongList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: queryFilter,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{
|
||||
cacheTime: 1000 * 60,
|
||||
|
@ -147,20 +141,7 @@ export const useHandlePlayQueueAdd = () => {
|
|||
|
||||
if (!songsList) return toast.warn({ message: 'No songs found' });
|
||||
|
||||
switch (server?.type) {
|
||||
case 'jellyfin':
|
||||
songs = songsList.items?.map((song) =>
|
||||
jfNormalize.song(song as JFSong, server, deviceId),
|
||||
);
|
||||
break;
|
||||
case 'navidrome':
|
||||
songs = songsList.items?.map((song) =>
|
||||
ndNormalize.song(song as NDSong, server, deviceId),
|
||||
);
|
||||
break;
|
||||
case 'subsonic':
|
||||
break;
|
||||
}
|
||||
songs = songsList.items?.map((song) => ({ ...song, uniqueId: nanoid() }));
|
||||
} else if (options.byData) {
|
||||
songs = options.byData.map((song) => ({ ...song, uniqueId: nanoid() }));
|
||||
}
|
||||
|
@ -207,7 +188,7 @@ export const useHandlePlayQueueAdd = () => {
|
|||
|
||||
return null;
|
||||
},
|
||||
[deviceId, play, playerType, queryClient, server],
|
||||
[play, playerType, queryClient, server],
|
||||
);
|
||||
|
||||
return handlePlayQueueAdd;
|
||||
|
|
|
@ -67,13 +67,13 @@ export const useScrobble = () => {
|
|||
currentSong?.serverType === ServerType.JELLYFIN ? currentTime * 1e7 : undefined;
|
||||
|
||||
sendScrobble.mutate({
|
||||
_serverId: currentSong?.serverId,
|
||||
query: {
|
||||
event: 'timeupdate',
|
||||
id: currentSong.id,
|
||||
position,
|
||||
submission: false,
|
||||
},
|
||||
serverId: currentSong?.serverId,
|
||||
});
|
||||
},
|
||||
[isScrobbleEnabled, sendScrobble],
|
||||
|
@ -110,12 +110,12 @@ export const useScrobble = () => {
|
|||
previousSong?.serverType === ServerType.JELLYFIN ? previousSongTime * 1e7 : undefined;
|
||||
|
||||
sendScrobble.mutate({
|
||||
_serverId: previousSong?.serverId,
|
||||
query: {
|
||||
id: previousSong.id,
|
||||
position,
|
||||
submission: true,
|
||||
},
|
||||
serverId: previousSong?.serverId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -130,13 +130,13 @@ export const useScrobble = () => {
|
|||
// Send start scrobble when song changes and the new song is playing
|
||||
if (status === PlayerStatus.PLAYING && currentSong?.id) {
|
||||
sendScrobble.mutate({
|
||||
_serverId: currentSong?.serverId,
|
||||
query: {
|
||||
event: 'start',
|
||||
id: currentSong.id,
|
||||
position: 0,
|
||||
submission: false,
|
||||
},
|
||||
serverId: currentSong?.serverId,
|
||||
});
|
||||
|
||||
if (currentSong?.serverType === ServerType.JELLYFIN) {
|
||||
|
@ -175,13 +175,13 @@ export const useScrobble = () => {
|
|||
// Whenever the player is restarted, send a 'start' scrobble
|
||||
if (status === PlayerStatus.PLAYING) {
|
||||
sendScrobble.mutate({
|
||||
_serverId: currentSong?.serverId,
|
||||
query: {
|
||||
event: 'unpause',
|
||||
id: currentSong.id,
|
||||
position,
|
||||
submission: false,
|
||||
},
|
||||
serverId: currentSong?.serverId,
|
||||
});
|
||||
|
||||
if (currentSong?.serverType === ServerType.JELLYFIN) {
|
||||
|
@ -194,13 +194,13 @@ export const useScrobble = () => {
|
|||
// Jellyfin is the only one that needs to send a 'pause' event to the server
|
||||
} else if (currentSong?.serverType === ServerType.JELLYFIN) {
|
||||
sendScrobble.mutate({
|
||||
_serverId: currentSong?.serverId,
|
||||
query: {
|
||||
event: 'pause',
|
||||
id: currentSong.id,
|
||||
position,
|
||||
submission: false,
|
||||
},
|
||||
serverId: currentSong?.serverId,
|
||||
});
|
||||
|
||||
if (progressIntervalId.current) {
|
||||
|
@ -217,11 +217,11 @@ export const useScrobble = () => {
|
|||
|
||||
if (!isCurrentSongScrobbled && shouldSubmitScrobble) {
|
||||
sendScrobble.mutate({
|
||||
_serverId: currentSong?.serverId,
|
||||
query: {
|
||||
id: currentSong.id,
|
||||
submission: true,
|
||||
},
|
||||
serverId: currentSong?.serverId,
|
||||
});
|
||||
|
||||
setIsCurrentSongScrobbled(true);
|
||||
|
@ -261,24 +261,24 @@ export const useScrobble = () => {
|
|||
|
||||
if (!isCurrentSongScrobbled && shouldSubmitScrobble) {
|
||||
sendScrobble.mutate({
|
||||
_serverId: currentSong?.serverId,
|
||||
query: {
|
||||
id: currentSong.id,
|
||||
position,
|
||||
submission: true,
|
||||
},
|
||||
serverId: currentSong?.serverId,
|
||||
});
|
||||
}
|
||||
|
||||
if (currentSong?.serverType === ServerType.JELLYFIN) {
|
||||
sendScrobble.mutate({
|
||||
_serverId: currentSong?.serverId,
|
||||
query: {
|
||||
event: 'start',
|
||||
id: currentSong.id,
|
||||
position: 0,
|
||||
submission: false,
|
||||
},
|
||||
serverId: currentSong?.serverId,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -23,15 +23,20 @@ export const AddToPlaylistContextModal = ({
|
|||
const server = useCurrentServer();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const addToPlaylistMutation = useAddToPlaylist();
|
||||
const addToPlaylistMutation = useAddToPlaylist({});
|
||||
|
||||
const playlistList = usePlaylistList({
|
||||
ndParams: {
|
||||
smart: false,
|
||||
query: {
|
||||
_custom: {
|
||||
navidrome: {
|
||||
smart: false,
|
||||
},
|
||||
},
|
||||
sortBy: PlaylistListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
},
|
||||
sortBy: PlaylistListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const playlistSelect = useMemo(() => {
|
||||
|
@ -60,11 +65,12 @@ export const AddToPlaylistContextModal = ({
|
|||
|
||||
const queryKey = queryKeys.songs.list(server?.id || '', query);
|
||||
|
||||
const songsRes = await queryClient.fetchQuery(queryKey, ({ signal }) =>
|
||||
api.controller.getSongList({ query, server, signal }),
|
||||
);
|
||||
const songsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => {
|
||||
if (!server) throw new Error('No server');
|
||||
return api.controller.getSongList({ apiClientProps: { server, signal }, query });
|
||||
});
|
||||
|
||||
return api.normalize.songList(songsRes, server);
|
||||
return songsRes;
|
||||
};
|
||||
|
||||
const getSongsByArtist = async (artistId: string) => {
|
||||
|
@ -77,11 +83,12 @@ export const AddToPlaylistContextModal = ({
|
|||
|
||||
const queryKey = queryKeys.songs.list(server?.id || '', query);
|
||||
|
||||
const songsRes = await queryClient.fetchQuery(queryKey, ({ signal }) =>
|
||||
api.controller.getSongList({ query, server, signal }),
|
||||
);
|
||||
const songsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => {
|
||||
if (!server) throw new Error('No server');
|
||||
return api.controller.getSongList({ apiClientProps: { server, signal }, query });
|
||||
});
|
||||
|
||||
return api.normalize.songList(songsRes, server);
|
||||
return songsRes;
|
||||
};
|
||||
|
||||
const isSubmitDisabled = form.values.playlistId.length === 0 || addToPlaylistMutation.isLoading;
|
||||
|
@ -118,17 +125,18 @@ export const AddToPlaylistContextModal = ({
|
|||
|
||||
const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId, query);
|
||||
|
||||
const playlistSongsRes = await queryClient.fetchQuery(queryKey, ({ signal }) =>
|
||||
api.controller.getPlaylistSongList({
|
||||
const playlistSongsRes = await queryClient.fetchQuery(queryKey, ({ signal }) => {
|
||||
if (!server) throw new Error('No server');
|
||||
return api.controller.getPlaylistSongList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: { id: playlistId, startIndex: 0 },
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const playlistSongIds = api.normalize
|
||||
.songList(playlistSongsRes, server)
|
||||
.items?.map((song) => song.id);
|
||||
const playlistSongIds = playlistSongsRes?.items?.map((song) => song.id);
|
||||
|
||||
for (const songId of allSongIds) {
|
||||
if (!playlistSongIds?.includes(songId)) {
|
||||
|
@ -138,10 +146,12 @@ export const AddToPlaylistContextModal = ({
|
|||
}
|
||||
|
||||
if (values.skipDuplicates ? uniqueSongIds.length > 0 : allSongIds.length > 0) {
|
||||
if (!server) return null;
|
||||
addToPlaylistMutation.mutate(
|
||||
{
|
||||
body: { songId: values.skipDuplicates ? uniqueSongIds : allSongIds },
|
||||
query: { id: playlistId },
|
||||
serverId: server?.id,
|
||||
},
|
||||
{
|
||||
onError: (err) => {
|
||||
|
|
|
@ -16,47 +16,54 @@ interface CreatePlaylistFormProps {
|
|||
}
|
||||
|
||||
export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
||||
const mutation = useCreatePlaylist();
|
||||
const mutation = useCreatePlaylist({});
|
||||
const server = useCurrentServer();
|
||||
const queryBuilderRef = useRef<PlaylistQueryBuilderRef>(null);
|
||||
|
||||
const form = useForm<CreatePlaylistBody>({
|
||||
initialValues: {
|
||||
_custom: {
|
||||
navidrome: {
|
||||
public: false,
|
||||
rules: undefined,
|
||||
},
|
||||
},
|
||||
comment: '',
|
||||
name: '',
|
||||
ndParams: {
|
||||
public: false,
|
||||
rules: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
const [isSmartPlaylist, setIsSmartPlaylist] = useState(false);
|
||||
|
||||
const handleSubmit = form.onSubmit((values) => {
|
||||
if (isSmartPlaylist) {
|
||||
values.ndParams = {
|
||||
...values.ndParams,
|
||||
values._custom!.navidrome = {
|
||||
...values._custom?.navidrome,
|
||||
rules: queryBuilderRef.current?.getFilters(),
|
||||
};
|
||||
}
|
||||
|
||||
const smartPlaylist = queryBuilderRef.current?.getFilters();
|
||||
|
||||
if (!server) return;
|
||||
|
||||
mutation.mutate(
|
||||
{
|
||||
body: {
|
||||
...values,
|
||||
ndParams: {
|
||||
...values.ndParams,
|
||||
rules:
|
||||
isSmartPlaylist && smartPlaylist?.filters
|
||||
? {
|
||||
...convertQueryGroupToNDQuery(smartPlaylist.filters),
|
||||
...smartPlaylist.extraFilters,
|
||||
}
|
||||
: undefined,
|
||||
_custom: {
|
||||
navidrome: {
|
||||
...values._custom?.navidrome,
|
||||
rules:
|
||||
isSmartPlaylist && smartPlaylist?.filters
|
||||
? {
|
||||
...convertQueryGroupToNDQuery(smartPlaylist.filters),
|
||||
...smartPlaylist.extraFilters,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
serverId: server.id,
|
||||
},
|
||||
{
|
||||
onError: (err) => {
|
||||
|
|
|
@ -17,16 +17,12 @@ import {
|
|||
UserListQuery,
|
||||
UserListSort,
|
||||
} from '/@/renderer/api/types';
|
||||
import { Button, ConfirmModal, DropdownMenu, MotionGroup, toast } from '/@/renderer/components';
|
||||
import {
|
||||
Button,
|
||||
ConfirmModal,
|
||||
DropdownMenu,
|
||||
getColumnDefs,
|
||||
MotionGroup,
|
||||
toast,
|
||||
useFixedTableHeader,
|
||||
VirtualTable,
|
||||
} from '/@/renderer/components';
|
||||
} from '/@/renderer/components/virtual-table';
|
||||
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
|
||||
import {
|
||||
PLAYLIST_SONG_CONTEXT_MENU_ITEMS,
|
||||
|
@ -68,19 +64,23 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
|
|||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const page = useSongListStore();
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const detailQuery = usePlaylistDetail({ id: playlistId });
|
||||
const server = useCurrentServer();
|
||||
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
const queryClient = useQueryClient();
|
||||
const server = useCurrentServer();
|
||||
|
||||
const playlistSongsQueryInfinite = usePlaylistSongListInfinite(
|
||||
{
|
||||
const playlistSongsQueryInfinite = usePlaylistSongListInfinite({
|
||||
options: {
|
||||
cacheTime: 0,
|
||||
keepPreviousData: false,
|
||||
},
|
||||
query: {
|
||||
id: playlistId,
|
||||
limit: 50,
|
||||
startIndex: 0,
|
||||
},
|
||||
{ cacheTime: 0, keepPreviousData: false },
|
||||
);
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const handleLoadMore = () => {
|
||||
playlistSongsQueryInfinite.fetchNextPage();
|
||||
|
@ -105,17 +105,17 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
|
|||
});
|
||||
|
||||
const playlistSongData = useMemo(
|
||||
() => playlistSongsQueryInfinite.data?.pages.flatMap((p) => p.items),
|
||||
() => playlistSongsQueryInfinite.data?.pages.flatMap((p) => p?.items),
|
||||
[playlistSongsQueryInfinite.data?.pages],
|
||||
);
|
||||
|
||||
const { intersectRef, tableContainerRef } = useFixedTableHeader();
|
||||
|
||||
const deletePlaylistMutation = useDeletePlaylist();
|
||||
const deletePlaylistMutation = useDeletePlaylist({});
|
||||
|
||||
const handleDeletePlaylist = () => {
|
||||
deletePlaylistMutation.mutate(
|
||||
{ query: { id: playlistId } },
|
||||
{ query: { id: playlistId }, serverId: server?.id },
|
||||
{
|
||||
onError: (err) => {
|
||||
toast.error({
|
||||
|
@ -165,30 +165,33 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps)
|
|||
startIndex: 0,
|
||||
};
|
||||
|
||||
if (!server) return;
|
||||
|
||||
const users = await queryClient.fetchQuery({
|
||||
queryFn: ({ signal }) => api.controller.getUserList({ query, server, signal }),
|
||||
queryFn: ({ signal }) =>
|
||||
api.controller.getUserList({ apiClientProps: { server, signal }, query }),
|
||||
queryKey: queryKeys.users.list(server?.id || '', query),
|
||||
});
|
||||
|
||||
const normalizedUsers = api.normalize.userList(users, server);
|
||||
|
||||
openModal({
|
||||
children: (
|
||||
<UpdatePlaylistForm
|
||||
body={{
|
||||
_custom: {
|
||||
navidrome: {
|
||||
owner: detailQuery?.data?.owner || undefined,
|
||||
ownerId: detailQuery?.data?.ownerId || undefined,
|
||||
public: detailQuery?.data?.public || false,
|
||||
rules: detailQuery?.data?.rules || undefined,
|
||||
sync: detailQuery?.data?.sync || undefined,
|
||||
},
|
||||
},
|
||||
comment: detailQuery?.data?.description || undefined,
|
||||
genres: detailQuery?.data?.genres,
|
||||
name: detailQuery?.data?.name,
|
||||
ndParams: {
|
||||
owner: detailQuery?.data?.owner || undefined,
|
||||
ownerId: detailQuery?.data?.ownerId || undefined,
|
||||
public: detailQuery?.data?.public || false,
|
||||
rules: detailQuery?.data?.rules || undefined,
|
||||
sync: detailQuery?.data?.sync || undefined,
|
||||
},
|
||||
}}
|
||||
query={{ id: playlistId }}
|
||||
users={normalizedUsers.items}
|
||||
users={users?.items}
|
||||
onCancel={closeAllModals}
|
||||
/>
|
||||
),
|
||||
|
|
|
@ -7,6 +7,7 @@ import { LibraryHeader } from '/@/renderer/features/shared';
|
|||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { formatDurationString } from '/@/renderer/utils';
|
||||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
import { useCurrentServer } from '../../../store/auth.store';
|
||||
|
||||
interface PlaylistDetailHeaderProps {
|
||||
background: string;
|
||||
|
@ -20,7 +21,8 @@ export const PlaylistDetailHeader = forwardRef(
|
|||
ref: Ref<HTMLDivElement>,
|
||||
) => {
|
||||
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 = [
|
||||
{
|
||||
|
|
|
@ -8,12 +8,6 @@ import type {
|
|||
RowDoubleClickedEvent,
|
||||
} from '@ag-grid-community/core';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import {
|
||||
getColumnDefs,
|
||||
TablePagination,
|
||||
VirtualGridAutoSizerContainer,
|
||||
VirtualTable,
|
||||
} from '/@/renderer/components';
|
||||
import {
|
||||
useCurrentServer,
|
||||
usePlaylistDetailStore,
|
||||
|
@ -44,6 +38,8 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
|
|||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
||||
import { getColumnDefs, VirtualTable, TablePagination } from '/@/renderer/components/virtual-table';
|
||||
|
||||
interface PlaylistDetailContentProps {
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
|
@ -61,7 +57,7 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
|
|||
};
|
||||
}, [page?.table.id, playlistId]);
|
||||
|
||||
const detailQuery = usePlaylistDetail({ id: playlistId });
|
||||
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
|
||||
|
||||
const p = usePlaylistDetailTablePagination(playlistId);
|
||||
const pagination = {
|
||||
|
@ -80,9 +76,12 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
|
|||
const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED;
|
||||
|
||||
const checkPlaylistList = usePlaylistSongList({
|
||||
id: playlistId,
|
||||
limit: 1,
|
||||
startIndex: 0,
|
||||
query: {
|
||||
id: playlistId,
|
||||
limit: 1,
|
||||
startIndex: 0,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const columnDefs: ColDef[] = useMemo(
|
||||
|
@ -104,24 +103,27 @@ export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailConten
|
|||
...filters,
|
||||
});
|
||||
|
||||
if (!server) return;
|
||||
|
||||
const songsRes = await queryClient.fetchQuery(
|
||||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getPlaylistSongList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
id: playlistId,
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
const songs = api.normalize.songList(songsRes, server);
|
||||
params.successCallback(songs?.items || [], songsRes?.totalRecordCount || 0);
|
||||
params.successCallback(songsRes?.items || [], songsRes?.totalRecordCount || 0);
|
||||
},
|
||||
rowCount: undefined,
|
||||
};
|
||||
|
|
|
@ -18,15 +18,7 @@ import {
|
|||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { LibraryItem, PlaylistSongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types';
|
||||
import {
|
||||
DropdownMenu,
|
||||
SONG_TABLE_COLUMNS,
|
||||
Button,
|
||||
Slider,
|
||||
MultiSelect,
|
||||
Switch,
|
||||
Text,
|
||||
} from '/@/renderer/components';
|
||||
import { DropdownMenu, Button, Slider, MultiSelect, Switch, Text } from '/@/renderer/components';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import {
|
||||
|
@ -41,6 +33,7 @@ import {
|
|||
import { ListDisplayType, ServerType, Play, TableColumn } from '/@/renderer/types';
|
||||
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
||||
import { useParams } from 'react-router';
|
||||
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||
|
||||
const FILTERS = {
|
||||
jellyfin: [
|
||||
|
@ -100,7 +93,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC,
|
||||
};
|
||||
|
||||
const detailQuery = usePlaylistDetail({ id: playlistId });
|
||||
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
|
||||
const isSmartPlaylist = detailQuery.data?.rules;
|
||||
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
|
@ -139,14 +132,16 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getPlaylistSongList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
id: playlistId,
|
||||
limit,
|
||||
startIndex,
|
||||
...filters,
|
||||
},
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
|
|
@ -8,6 +8,7 @@ import { usePlayQueueAdd } from '/@/renderer/features/player';
|
|||
import { PlaylistDetailSongListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header-filters';
|
||||
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
||||
import { LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { Play } from '/@/renderer/types';
|
||||
|
||||
|
@ -23,7 +24,8 @@ export const PlaylistDetailSongListHeader = ({
|
|||
handleToggleShowQueryBuilder,
|
||||
}: PlaylistDetailHeaderProps) => {
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const detailQuery = usePlaylistDetail({ id: playlistId });
|
||||
const server = useCurrentServer();
|
||||
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
|
||||
const handlePlay = async (playType: Play) => {
|
||||
|
|
|
@ -12,12 +12,6 @@ import { Stack } from '@mantine/core';
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import {
|
||||
getColumnDefs,
|
||||
TablePagination,
|
||||
VirtualGridAutoSizerContainer,
|
||||
VirtualTable,
|
||||
} from '/@/renderer/components';
|
||||
import {
|
||||
useCurrentServer,
|
||||
usePlaylistListStore,
|
||||
|
@ -33,6 +27,8 @@ import { PLAYLIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/c
|
|||
import { generatePath, useNavigate } from 'react-router';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
||||
import { getColumnDefs, VirtualTable, TablePagination } from '/@/renderer/components/virtual-table';
|
||||
|
||||
interface PlaylistListContentProps {
|
||||
itemCount?: number;
|
||||
|
@ -81,13 +77,15 @@ export const PlaylistListContent = ({ tableRef, itemCount }: PlaylistListContent
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getPlaylistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...page.filter,
|
||||
},
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
|
|
@ -7,15 +7,7 @@ import { RiSortAsc, RiSortDesc, RiMoreFill, RiRefreshLine, RiSettings3Fill } fro
|
|||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { SortOrder, PlaylistListSort } from '/@/renderer/api/types';
|
||||
import {
|
||||
DropdownMenu,
|
||||
PLAYLIST_TABLE_COLUMNS,
|
||||
Text,
|
||||
Button,
|
||||
Slider,
|
||||
MultiSelect,
|
||||
Switch,
|
||||
} from '/@/renderer/components';
|
||||
import { DropdownMenu, Text, Button, Slider, MultiSelect, Switch } from '/@/renderer/components';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import {
|
||||
PlaylistListFilter,
|
||||
|
@ -27,6 +19,7 @@ import {
|
|||
useSetPlaylistTablePagination,
|
||||
} from '/@/renderer/store';
|
||||
import { ListDisplayType, TableColumn } from '/@/renderer/types';
|
||||
import { PLAYLIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||
|
||||
const FILTERS = {
|
||||
jellyfin: [
|
||||
|
@ -91,13 +84,15 @@ export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFilter
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getPlaylistList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query: {
|
||||
limit,
|
||||
startIndex,
|
||||
...pageFilters,
|
||||
},
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Group, Stack } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { CreatePlaylistBody, RawCreatePlaylistResponse, ServerType } from '/@/renderer/api/types';
|
||||
import { CreatePlaylistBody, CreatePlaylistResponse, ServerType } from '/@/renderer/api/types';
|
||||
import { Button, Switch, TextInput, toast } from '/@/renderer/components';
|
||||
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
|
@ -8,22 +8,24 @@ import { useCurrentServer } from '/@/renderer/store';
|
|||
interface SaveAsPlaylistFormProps {
|
||||
body: Partial<CreatePlaylistBody>;
|
||||
onCancel: () => void;
|
||||
onSuccess: (data: RawCreatePlaylistResponse) => void;
|
||||
onSuccess: (data: CreatePlaylistResponse) => void;
|
||||
}
|
||||
|
||||
export const SaveAsPlaylistForm = ({ body, onSuccess, onCancel }: SaveAsPlaylistFormProps) => {
|
||||
const mutation = useCreatePlaylist();
|
||||
const mutation = useCreatePlaylist({});
|
||||
const server = useCurrentServer();
|
||||
|
||||
const form = useForm<CreatePlaylistBody>({
|
||||
initialValues: {
|
||||
_custom: {
|
||||
navidrome: {
|
||||
public: false,
|
||||
rules: undefined,
|
||||
...body?._custom?.navidrome,
|
||||
},
|
||||
},
|
||||
comment: body.comment || '',
|
||||
name: body.name || '',
|
||||
ndParams: {
|
||||
public: false,
|
||||
rules: undefined,
|
||||
...body.ndParams,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ interface UpdatePlaylistFormProps {
|
|||
}
|
||||
|
||||
export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlaylistFormProps) => {
|
||||
const mutation = useUpdatePlaylist();
|
||||
const mutation = useUpdatePlaylist({});
|
||||
const server = useCurrentServer();
|
||||
|
||||
const userList = users?.map((user) => ({
|
||||
|
@ -23,21 +23,27 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
|
|||
|
||||
const form = useForm<UpdatePlaylistBody>({
|
||||
initialValues: {
|
||||
_custom: {
|
||||
navidrome: {
|
||||
owner: body?._custom?.navidrome?.owner || '',
|
||||
ownerId: body?._custom?.navidrome?.ownerId || '',
|
||||
public: body?._custom?.navidrome?.public || false,
|
||||
rules: undefined,
|
||||
sync: body?._custom?.navidrome?.sync || false,
|
||||
},
|
||||
},
|
||||
comment: body?.comment || '',
|
||||
name: body?.name || '',
|
||||
ndParams: {
|
||||
owner: body?.ndParams?.owner || '',
|
||||
ownerId: body?.ndParams?.ownerId || '',
|
||||
public: body?.ndParams?.public || false,
|
||||
rules: undefined,
|
||||
sync: body?.ndParams?.sync || false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = form.onSubmit((values) => {
|
||||
mutation.mutate(
|
||||
{ body: values, query },
|
||||
{
|
||||
body: values,
|
||||
query,
|
||||
serverId: server?.id,
|
||||
},
|
||||
{
|
||||
onError: (err) => {
|
||||
toast.error({ message: err.message, title: 'Error updating playlist' });
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { useRef } from 'react';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { useParams } from 'react-router';
|
||||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
import { NativeScrollArea } from '/@/renderer/components';
|
||||
|
@ -10,14 +10,16 @@ import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playli
|
|||
import { AnimatedPage, LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||
import { useFastAverageColor } from '/@/renderer/hooks';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { useCurrentServer } from '../../../store/auth.store';
|
||||
|
||||
const PlaylistDetailRoute = () => {
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
const scrollAreaRef = useRef<HTMLDivElement>(null);
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const server = useCurrentServer();
|
||||
|
||||
const detailQuery = usePlaylistDetail({ id: playlistId });
|
||||
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
|
||||
const background = useFastAverageColor(
|
||||
detailQuery?.data?.imageUrl,
|
||||
!detailQuery?.isLoading,
|
||||
|
|
|
@ -23,11 +23,11 @@ const PlaylistDetailSongListRoute = () => {
|
|||
const navigate = useNavigate();
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const currentServer = useCurrentServer();
|
||||
const server = useCurrentServer();
|
||||
|
||||
const detailQuery = usePlaylistDetail({ id: playlistId });
|
||||
const createPlaylistMutation = useCreatePlaylist();
|
||||
const deletePlaylistMutation = useDeletePlaylist();
|
||||
const detailQuery = usePlaylistDetail({ query: { id: playlistId }, serverId: server?.id });
|
||||
const createPlaylistMutation = useCreatePlaylist({});
|
||||
const deletePlaylistMutation = useDeletePlaylist({});
|
||||
|
||||
const handleSave = (
|
||||
filter: Record<string, any>,
|
||||
|
@ -45,15 +45,17 @@ const PlaylistDetailSongListRoute = () => {
|
|||
createPlaylistMutation.mutate(
|
||||
{
|
||||
body: {
|
||||
_custom: {
|
||||
navidrome: {
|
||||
owner: detailQuery?.data?.owner || '',
|
||||
ownerId: detailQuery?.data?.ownerId || '',
|
||||
public: detailQuery?.data?.public || false,
|
||||
rules,
|
||||
sync: detailQuery?.data?.sync || false,
|
||||
},
|
||||
},
|
||||
comment: detailQuery?.data?.description || '',
|
||||
name: detailQuery?.data?.name,
|
||||
ndParams: {
|
||||
owner: detailQuery?.data?.owner || '',
|
||||
ownerId: detailQuery?.data?.ownerId || '',
|
||||
public: detailQuery?.data?.public || false,
|
||||
rules,
|
||||
sync: detailQuery?.data?.sync || false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -73,19 +75,21 @@ const PlaylistDetailSongListRoute = () => {
|
|||
children: (
|
||||
<SaveAsPlaylistForm
|
||||
body={{
|
||||
_custom: {
|
||||
navidrome: {
|
||||
owner: detailQuery?.data?.owner || '',
|
||||
ownerId: detailQuery?.data?.ownerId || '',
|
||||
public: detailQuery?.data?.public || false,
|
||||
rules: {
|
||||
...filter,
|
||||
order: 'desc',
|
||||
sort: 'year',
|
||||
},
|
||||
sync: detailQuery?.data?.sync || false,
|
||||
},
|
||||
},
|
||||
comment: detailQuery?.data?.description || '',
|
||||
name: detailQuery?.data?.name,
|
||||
ndParams: {
|
||||
owner: detailQuery?.data?.owner || '',
|
||||
ownerId: detailQuery?.data?.ownerId || '',
|
||||
public: detailQuery?.data?.public || false,
|
||||
rules: {
|
||||
...filter,
|
||||
order: 'desc',
|
||||
sort: 'year',
|
||||
},
|
||||
sync: detailQuery?.data?.sync || false,
|
||||
},
|
||||
}}
|
||||
onCancel={closeAllModals}
|
||||
onSuccess={(data) =>
|
||||
|
@ -120,9 +124,7 @@ const PlaylistDetailSongListRoute = () => {
|
|||
};
|
||||
|
||||
const isSmartPlaylist =
|
||||
!detailQuery?.isLoading &&
|
||||
detailQuery?.data?.rules &&
|
||||
currentServer?.type === ServerType.NAVIDROME;
|
||||
!detailQuery?.isLoading && detailQuery?.data?.rules && server?.type === ServerType.NAVIDROME;
|
||||
|
||||
const [showQueryBuilder, setShowQueryBuilder] = useState(false);
|
||||
const [isQueryBuilderExpanded, setIsQueryBuilderExpanded] = useState(false);
|
||||
|
@ -142,18 +144,19 @@ const PlaylistDetailSongListRoute = () => {
|
|||
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC,
|
||||
};
|
||||
|
||||
const itemCountCheck = usePlaylistSongList(
|
||||
{
|
||||
const itemCountCheck = usePlaylistSongList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 60 * 2,
|
||||
},
|
||||
query: {
|
||||
id: playlistId,
|
||||
limit: 1,
|
||||
startIndex: 0,
|
||||
...filters,
|
||||
},
|
||||
{
|
||||
cacheTime: 1000 * 60 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 60 * 2,
|
||||
},
|
||||
);
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const itemCount =
|
||||
itemCountCheck.data?.totalRecordCount === null
|
||||
|
|
|
@ -9,18 +9,18 @@ import { AnimatedPage } from '/@/renderer/features/shared';
|
|||
const PlaylistListRoute = () => {
|
||||
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,
|
||||
sortBy: PlaylistListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
},
|
||||
{
|
||||
cacheTime: 1000 * 60 * 60 * 2,
|
||||
staleTime: 1000 * 60 * 60 * 2,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const itemCount =
|
||||
itemCountCheck.data?.totalRecordCount === null
|
||||
|
|
|
@ -77,7 +77,7 @@ export const AddServerForm = ({ onCancel }: AddServerFormProps) => {
|
|||
setCurrentServer(serverItem);
|
||||
closeAllModals();
|
||||
|
||||
if (serverList.length === 0) {
|
||||
if (Object.keys(serverList).length === 0) {
|
||||
toast.success({ message: 'Server has been added, reloading...' });
|
||||
setTimeout(() => window.location.reload(), 2000);
|
||||
} else {
|
||||
|
|
|
@ -62,6 +62,7 @@ export const ServerList = () => {
|
|||
position: 'absolute',
|
||||
right: 55,
|
||||
transform: 'translateY(-3.5rem)',
|
||||
zIndex: 2000,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
|
@ -77,21 +78,24 @@ export const ServerList = () => {
|
|||
</Group>
|
||||
<Stack>
|
||||
<Accordion variant="separated">
|
||||
{serverListQuery?.map((s) => (
|
||||
<Accordion.Item
|
||||
key={s.id}
|
||||
value={s.name}
|
||||
>
|
||||
<Accordion.Control icon={<RiServerFill size={15} />}>
|
||||
<Group position="apart">
|
||||
{titleCase(s.type)} - {s.name}
|
||||
</Group>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<ServerListItem server={s} />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
))}
|
||||
{Object.keys(serverListQuery)?.map((serverId) => {
|
||||
const server = serverListQuery[serverId];
|
||||
return (
|
||||
<Accordion.Item
|
||||
key={server.id}
|
||||
value={server.name}
|
||||
>
|
||||
<Accordion.Control icon={<RiServerFill size={15} />}>
|
||||
<Group position="apart">
|
||||
{titleCase(server?.type)} - {server?.name}
|
||||
</Group>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<ServerListItem server={server} />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
);
|
||||
})}
|
||||
</Accordion>
|
||||
<Divider />
|
||||
<Group>
|
||||
|
|
|
@ -6,5 +6,5 @@ export * from './components/library-header';
|
|||
export * from './components/library-header-bar';
|
||||
export * from './mutations/create-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';
|
||||
|
|
|
@ -100,9 +100,12 @@ export const Sidebar = () => {
|
|||
};
|
||||
|
||||
const playlistsQuery = usePlaylistList({
|
||||
sortBy: PlaylistListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
query: {
|
||||
sortBy: PlaylistListSort.NAME,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
},
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
|
||||
|
|
|
@ -9,30 +9,32 @@ interface JellyfinSongFiltersProps {
|
|||
handleFilterChange: (filters: SongListFilter) => void;
|
||||
id?: string;
|
||||
pageKey: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const JellyfinSongFilters = ({
|
||||
id,
|
||||
pageKey,
|
||||
handleFilterChange,
|
||||
serverId,
|
||||
}: JellyfinSongFiltersProps) => {
|
||||
const { setFilter } = useListStoreActions();
|
||||
const filter = useSongListFilter({ id, key: pageKey });
|
||||
|
||||
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
|
||||
const genreListQuery = useGenreList(null);
|
||||
const genreListQuery = useGenreList({ query: null, serverId });
|
||||
|
||||
const genreList = useMemo(() => {
|
||||
if (!genreListQuery?.data) return [];
|
||||
return genreListQuery.data.map((genre) => ({
|
||||
return genreListQuery.data.items.map((genre) => ({
|
||||
label: genre.name,
|
||||
value: genre.id,
|
||||
}));
|
||||
}, [genreListQuery.data]);
|
||||
|
||||
const selectedGenres = useMemo(() => {
|
||||
return filter.jfParams?.genreIds?.split(',');
|
||||
}, [filter.jfParams?.genreIds]);
|
||||
return filter._custom?.jellyfin?.genreIds?.split(',');
|
||||
}, [filter._custom?.jellyfin?.genreIds]);
|
||||
|
||||
const toggleFilters = [
|
||||
{
|
||||
|
@ -40,17 +42,20 @@ export const JellyfinSongFilters = ({
|
|||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
jfParams: {
|
||||
...filter.jfParams,
|
||||
includeItemTypes: 'Audio',
|
||||
isFavorite: e.currentTarget.checked ? true : undefined,
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
jellyfin: {
|
||||
...filter._custom?.jellyfin,
|
||||
includeItemTypes: 'Audio',
|
||||
isFavorite: e.currentTarget.checked ? true : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
}) as SongListFilter;
|
||||
handleFilterChange(updatedFilters);
|
||||
},
|
||||
value: filter.jfParams?.isFavorite,
|
||||
value: filter._custom?.jellyfin?.isFavorite,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -58,10 +63,13 @@ export const JellyfinSongFilters = ({
|
|||
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
jfParams: {
|
||||
...filter.jfParams,
|
||||
includeItemTypes: 'Audio',
|
||||
minYear: e === '' ? undefined : (e as number),
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
jellyfin: {
|
||||
...filter._custom?.jellyfin,
|
||||
includeItemTypes: 'Audio',
|
||||
minYear: e === '' ? undefined : (e as number),
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
|
@ -73,10 +81,13 @@ export const JellyfinSongFilters = ({
|
|||
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
jfParams: {
|
||||
...filter.jfParams,
|
||||
includeItemTypes: 'Audio',
|
||||
maxYear: e === '' ? undefined : (e as number),
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
jellyfin: {
|
||||
...filter._custom?.jellyfin,
|
||||
includeItemTypes: 'Audio',
|
||||
maxYear: e === '' ? undefined : (e as number),
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
|
@ -88,10 +99,13 @@ export const JellyfinSongFilters = ({
|
|||
const genreFilterString = e?.length ? e.join(',') : undefined;
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
jfParams: {
|
||||
...filter.jfParams,
|
||||
genreIds: genreFilterString,
|
||||
includeItemTypes: 'Audio',
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
jellyfin: {
|
||||
...filter._custom?.jellyfin,
|
||||
genreIds: genreFilterString,
|
||||
includeItemTypes: 'Audio',
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
|
@ -117,14 +131,14 @@ export const JellyfinSongFilters = ({
|
|||
<Group grow>
|
||||
<NumberInput
|
||||
required
|
||||
defaultValue={filter.jfParams?.minYear}
|
||||
defaultValue={filter._custom?.jellyfin?.minYear}
|
||||
label="From year"
|
||||
max={2300}
|
||||
min={1700}
|
||||
onChange={handleMinYearFilter}
|
||||
/>
|
||||
<NumberInput
|
||||
defaultValue={filter.jfParams?.maxYear}
|
||||
defaultValue={filter._custom?.jellyfin?.maxYear}
|
||||
label="To year"
|
||||
max={2300}
|
||||
min={1700}
|
||||
|
|
|
@ -9,21 +9,23 @@ interface NavidromeSongFiltersProps {
|
|||
handleFilterChange: (filters: SongListFilter) => void;
|
||||
id?: string;
|
||||
pageKey: string;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export const NavidromeSongFilters = ({
|
||||
handleFilterChange,
|
||||
pageKey,
|
||||
id,
|
||||
serverId,
|
||||
}: NavidromeSongFiltersProps) => {
|
||||
const { setFilter } = useListStoreActions();
|
||||
const filter = useSongListFilter({ id, key: pageKey });
|
||||
|
||||
const genreListQuery = useGenreList(null);
|
||||
const genreListQuery = useGenreList({ query: null, serverId });
|
||||
|
||||
const genreList = useMemo(() => {
|
||||
if (!genreListQuery?.data) return [];
|
||||
return genreListQuery.data.map((genre) => ({
|
||||
return genreListQuery.data.items.map((genre) => ({
|
||||
label: genre.name,
|
||||
value: genre.id,
|
||||
}));
|
||||
|
@ -32,9 +34,11 @@ export const NavidromeSongFilters = ({
|
|||
const handleGenresFilter = debounce((e: string | null) => {
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
ndParams: {
|
||||
...filter.ndParams,
|
||||
genre_id: e || undefined,
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
navidrome: {
|
||||
genre_id: e || undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
|
@ -48,23 +52,30 @@ export const NavidromeSongFilters = ({
|
|||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
ndParams: { ...filter.ndParams, starred: e.currentTarget.checked ? true : undefined },
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
navidrome: {
|
||||
starred: e.currentTarget.checked ? true : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
}) as SongListFilter;
|
||||
|
||||
handleFilterChange(updatedFilters);
|
||||
},
|
||||
value: filter.ndParams?.starred,
|
||||
value: filter._custom?.navidrome?.starred,
|
||||
},
|
||||
];
|
||||
|
||||
const handleYearFilter = debounce((e: number | string) => {
|
||||
const updatedFilters = setFilter({
|
||||
data: {
|
||||
ndParams: {
|
||||
...filter.ndParams,
|
||||
year: e === '' ? undefined : (e as number),
|
||||
_custom: {
|
||||
...filter._custom,
|
||||
navidrome: {
|
||||
year: e === '' ? undefined : (e as number),
|
||||
},
|
||||
},
|
||||
},
|
||||
key: pageKey,
|
||||
|
@ -94,7 +105,7 @@ export const NavidromeSongFilters = ({
|
|||
label="Year"
|
||||
max={5000}
|
||||
min={0}
|
||||
value={filter.ndParams?.year}
|
||||
value={filter._custom?.navidrome?.year}
|
||||
width={50}
|
||||
onChange={(e) => handleYearFilter(e)}
|
||||
/>
|
||||
|
@ -102,7 +113,7 @@ export const NavidromeSongFilters = ({
|
|||
clearable
|
||||
searchable
|
||||
data={genreList}
|
||||
defaultValue={filter.ndParams?.genre_id}
|
||||
defaultValue={filter._custom?.navidrome?.genre_id}
|
||||
label="Genre"
|
||||
width={150}
|
||||
onChange={handleGenresFilter}
|
||||
|
|
|
@ -12,12 +12,6 @@ import { Stack } from '@mantine/core';
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import {
|
||||
getColumnDefs,
|
||||
TablePagination,
|
||||
VirtualGridAutoSizerContainer,
|
||||
VirtualTable,
|
||||
} from '/@/renderer/components';
|
||||
import {
|
||||
useCurrentServer,
|
||||
useListStoreActions,
|
||||
|
@ -33,6 +27,8 @@ import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
|||
import { LibraryItem, QueueSong, SongListQuery } from '/@/renderer/api/types';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
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 {
|
||||
itemCount?: number;
|
||||
|
@ -74,9 +70,11 @@ export const SongListContent = ({ itemCount, tableRef }: SongListContentProps) =
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getSongList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
|
|
@ -17,15 +17,7 @@ import {
|
|||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { LibraryItem, SongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types';
|
||||
import {
|
||||
DropdownMenu,
|
||||
SONG_TABLE_COLUMNS,
|
||||
Button,
|
||||
Slider,
|
||||
MultiSelect,
|
||||
Switch,
|
||||
Text,
|
||||
} from '/@/renderer/components';
|
||||
import { DropdownMenu, Button, Slider, MultiSelect, Switch, Text } from '/@/renderer/components';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { useMusicFolders } from '/@/renderer/features/shared';
|
||||
import { JellyfinSongFilters } from '/@/renderer/features/songs/components/jellyfin-song-filters';
|
||||
|
@ -42,6 +34,7 @@ import {
|
|||
} from '/@/renderer/store';
|
||||
import { ListDisplayType, ServerType, Play, TableColumn } from '/@/renderer/types';
|
||||
import { useSongListContext } from '/@/renderer/features/songs/context/song-list-context';
|
||||
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||
|
||||
const FILTERS = {
|
||||
jellyfin: [
|
||||
|
@ -100,7 +93,7 @@ export const SongListHeaderFilters = ({
|
|||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const musicFoldersQuery = useMusicFolders();
|
||||
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id });
|
||||
|
||||
const sortByLabel =
|
||||
(server?.type &&
|
||||
|
@ -133,9 +126,11 @@ export const SongListHeaderFilters = ({
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getSongList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
@ -306,18 +301,18 @@ export const SongListHeaderFilters = ({
|
|||
const isFilterApplied = useMemo(() => {
|
||||
const isNavidromeFilterApplied =
|
||||
server?.type === ServerType.NAVIDROME &&
|
||||
filter.ndParams &&
|
||||
Object.values(filter.ndParams).some((value) => value !== undefined);
|
||||
filter._custom?.navidrome &&
|
||||
Object.values(filter._custom?.navidrome).some((value) => value !== undefined);
|
||||
|
||||
const isJellyfinFilterApplied =
|
||||
server?.type === ServerType.JELLYFIN &&
|
||||
filter.jfParams &&
|
||||
Object.values(filter.jfParams)
|
||||
filter._custom?.jellyfin &&
|
||||
Object.values(filter._custom?.jellyfin)
|
||||
.filter((value) => value !== 'Audio') // Don't account for includeItemTypes: Audio
|
||||
.some((value) => value !== undefined);
|
||||
|
||||
return isNavidromeFilterApplied || isJellyfinFilterApplied;
|
||||
}, [filter.jfParams, filter.ndParams, server?.type]);
|
||||
}, [filter._custom?.jellyfin, filter._custom?.navidrome, server?.type]);
|
||||
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
|
@ -382,7 +377,7 @@ export const SongListHeaderFilters = ({
|
|||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{musicFoldersQuery.data?.map((folder) => (
|
||||
{musicFoldersQuery.data?.items.map((folder) => (
|
||||
<DropdownMenu.Item
|
||||
key={`musicFolder-${folder.id}`}
|
||||
$isActive={filter.musicFolderId === folder.id}
|
||||
|
|
|
@ -64,15 +64,16 @@ export const SongListHeader = ({
|
|||
queryKey,
|
||||
async ({ signal }) =>
|
||||
api.controller.getSongList({
|
||||
apiClientProps: {
|
||||
server,
|
||||
signal,
|
||||
},
|
||||
query,
|
||||
server,
|
||||
signal,
|
||||
}),
|
||||
{ cacheTime: 1000 * 60 * 1 },
|
||||
);
|
||||
|
||||
const songs = api.normalize.songList(songsRes, server);
|
||||
params.successCallback(songs?.items || [], songsRes?.totalRecordCount || 0);
|
||||
params.successCallback(songsRes?.items || [], songsRes?.totalRecordCount || 0);
|
||||
},
|
||||
rowCount: undefined,
|
||||
};
|
||||
|
|
|
@ -19,17 +19,18 @@ const TrackListRoute = () => {
|
|||
);
|
||||
|
||||
const songListFilter = useSongListFilter({ id: albumArtistId, key: pageKey });
|
||||
const itemCountCheck = useSongList(
|
||||
{
|
||||
const itemCountCheck = useSongList({
|
||||
options: {
|
||||
cacheTime: 1000 * 60,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
query: {
|
||||
limit: 1,
|
||||
startIndex: 0,
|
||||
...songListFilter,
|
||||
},
|
||||
{
|
||||
cacheTime: 1000 * 60,
|
||||
staleTime: 1000 * 60,
|
||||
},
|
||||
);
|
||||
serverId: server?.id,
|
||||
});
|
||||
|
||||
const itemCount =
|
||||
itemCountCheck.data?.totalRecordCount === null
|
||||
|
|
|
@ -60,22 +60,23 @@ export const AppMenu = () => {
|
|||
return (
|
||||
<>
|
||||
<DropdownMenu.Label>Select a server</DropdownMenu.Label>
|
||||
{serverList.map((s) => {
|
||||
const isNavidromeExpired = s.type === ServerType.NAVIDROME && !s.ndCredential;
|
||||
{Object.keys(serverList).map((serverId) => {
|
||||
const server = serverList[serverId];
|
||||
const isNavidromeExpired = server.type === ServerType.NAVIDROME && !server.ndCredential;
|
||||
const isJellyfinExpired = false;
|
||||
const isSessionExpired = isNavidromeExpired || isJellyfinExpired;
|
||||
|
||||
return (
|
||||
<DropdownMenu.Item
|
||||
key={`server-${s.id}`}
|
||||
$isActive={s.id === currentServer?.id}
|
||||
key={`server-${server.id}`}
|
||||
$isActive={server.id === currentServer?.id}
|
||||
icon={isSessionExpired ? <RiLockLine color="var(--danger-color)" /> : <RiServerFill />}
|
||||
onClick={() => {
|
||||
if (!isSessionExpired) return handleSetCurrentServer(s);
|
||||
return handleCredentialsModal(s);
|
||||
if (!isSessionExpired) return handleSetCurrentServer(server);
|
||||
return handleCredentialsModal(server);
|
||||
}}
|
||||
>
|
||||
<Group>{s.name}</Group>
|
||||
<Group>{server.name}</Group>
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
})}
|
||||
|
|
Reference in a new issue