Update album list implementation
This commit is contained in:
parent
55937e71db
commit
6dd9333dbb
9 changed files with 311 additions and 589 deletions
|
@ -2,8 +2,8 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
|
||||||
import { lazy, MutableRefObject, Suspense } from 'react';
|
import { lazy, MutableRefObject, Suspense } from 'react';
|
||||||
import { Spinner } from '/@/renderer/components';
|
import { Spinner } from '/@/renderer/components';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { useAlbumListStore } from '/@/renderer/store';
|
import { useListStoreByKey } from '/@/renderer/store';
|
||||||
import { ListDisplayType } from '/@/renderer/types';
|
import { ListDisplayType } from '/@/renderer/types';
|
||||||
|
|
||||||
const AlbumListGridView = lazy(() =>
|
const AlbumListGridView = lazy(() =>
|
||||||
|
@ -25,8 +25,8 @@ interface AlbumListContentProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListContentProps) => {
|
export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListContentProps) => {
|
||||||
const { id, pageKey } = useAlbumListContext();
|
const { pageKey } = useListContext();
|
||||||
const { display } = useAlbumListStore({ id, key: pageKey });
|
const { display } = useListStoreByKey({ key: pageKey });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Spinner container />}>
|
<Suspense fallback={<Spinner container />}>
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
|
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { ListOnScrollProps } from 'react-window';
|
import { ListOnScrollProps } from 'react-window';
|
||||||
import { controller } from '/@/renderer/api/controller';
|
import { controller } from '/@/renderer/api/controller';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys, splitPaginatedQuery } from '/@/renderer/api/query-keys';
|
||||||
import { Album, AlbumListQuery, AlbumListSort, LibraryItem } from '/@/renderer/api/types';
|
import {
|
||||||
|
Album,
|
||||||
|
AlbumListQuery,
|
||||||
|
AlbumListResponse,
|
||||||
|
AlbumListSort,
|
||||||
|
LibraryItem,
|
||||||
|
} from '/@/renderer/api/types';
|
||||||
import { ALBUM_CARD_ROWS } from '/@/renderer/components';
|
import { ALBUM_CARD_ROWS } from '/@/renderer/components';
|
||||||
import {
|
import {
|
||||||
VirtualGridAutoSizerContainer,
|
VirtualGridAutoSizerContainer,
|
||||||
VirtualInfiniteGrid,
|
VirtualInfiniteGrid,
|
||||||
} from '/@/renderer/components/virtual-grid';
|
} from '/@/renderer/components/virtual-grid';
|
||||||
import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
|
||||||
import {
|
|
||||||
useAlbumListFilter,
|
|
||||||
useAlbumListStore,
|
|
||||||
useCurrentServer,
|
|
||||||
useListStoreActions,
|
|
||||||
} from '/@/renderer/store';
|
|
||||||
import { CardRow, ListDisplayType } from '/@/renderer/types';
|
|
||||||
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
|
import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared';
|
||||||
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
|
import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
|
||||||
|
import { CardRow, ListDisplayType } from '/@/renderer/types';
|
||||||
|
|
||||||
export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
|
export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
const { id, pageKey } = useAlbumListContext();
|
const { pageKey, customFilters } = useListContext();
|
||||||
const { grid, display } = useAlbumListStore({ id, key: pageKey });
|
const { grid, display, filter } = useListStoreByKey({ key: pageKey });
|
||||||
const { setGrid } = useListStoreActions();
|
const { setGrid } = useListStoreActions();
|
||||||
const filter = useAlbumListFilter({ id, key: pageKey });
|
|
||||||
|
|
||||||
const createFavoriteMutation = useCreateFavorite({});
|
const createFavoriteMutation = useCreateFavorite({});
|
||||||
const deleteFavoriteMutation = useDeleteFavorite({});
|
const deleteFavoriteMutation = useDeleteFavorite({});
|
||||||
|
@ -129,27 +129,56 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
|
||||||
[pageKey, setGrid],
|
[pageKey, setGrid],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const fetchInitialData = useCallback(() => {
|
||||||
|
const query: Omit<AlbumListQuery, 'startIndex' | 'limit'> = {
|
||||||
|
...filter,
|
||||||
|
...customFilters,
|
||||||
|
};
|
||||||
|
|
||||||
|
const queriesFromCache: [QueryKey, AlbumListResponse][] = queryClient.getQueriesData({
|
||||||
|
exact: false,
|
||||||
|
fetchStatus: 'idle',
|
||||||
|
queryKey: queryKeys.albums.list(server?.id || '', query),
|
||||||
|
stale: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const itemData = [];
|
||||||
|
|
||||||
|
for (const [, data] of queriesFromCache) {
|
||||||
|
const { items, startIndex } = data || {};
|
||||||
|
|
||||||
|
if (items && startIndex !== undefined) {
|
||||||
|
let itemIndex = 0;
|
||||||
|
for (
|
||||||
|
let rowIndex = startIndex;
|
||||||
|
rowIndex < startIndex + items.length;
|
||||||
|
rowIndex += 1
|
||||||
|
) {
|
||||||
|
itemData[rowIndex] = items[itemIndex];
|
||||||
|
itemIndex += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemData;
|
||||||
|
}, [customFilters, filter, queryClient, server?.id]);
|
||||||
|
|
||||||
const fetch = useCallback(
|
const fetch = useCallback(
|
||||||
async ({ skip, take }: { skip: number; take: number }) => {
|
async ({ skip, take }: { skip: number; take: number }) => {
|
||||||
if (!server) {
|
if (!server) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const query: AlbumListQuery = {
|
const listQuery: AlbumListQuery = {
|
||||||
limit: take,
|
limit: take,
|
||||||
startIndex: skip,
|
startIndex: skip,
|
||||||
...filter,
|
...filter,
|
||||||
_custom: {
|
...customFilters,
|
||||||
jellyfin: {
|
|
||||||
...filter._custom?.jellyfin,
|
|
||||||
},
|
|
||||||
navidrome: {
|
|
||||||
...filter._custom?.navidrome,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const queryKey = queryKeys.albums.list(server?.id || '', query);
|
const { query, pagination } = splitPaginatedQuery(listQuery);
|
||||||
|
|
||||||
|
const queryKey = queryKeys.albums.list(server?.id || '', query, pagination);
|
||||||
|
|
||||||
const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) =>
|
const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) =>
|
||||||
controller.getAlbumList({
|
controller.getAlbumList({
|
||||||
|
@ -157,13 +186,13 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
|
||||||
server,
|
server,
|
||||||
signal,
|
signal,
|
||||||
},
|
},
|
||||||
query,
|
query: listQuery,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return albums;
|
return albums;
|
||||||
},
|
},
|
||||||
[filter, queryClient, server],
|
[customFilters, filter, queryClient, server],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -176,6 +205,7 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
|
||||||
cardRows={cardRows}
|
cardRows={cardRows}
|
||||||
display={display || ListDisplayType.CARD}
|
display={display || ListDisplayType.CARD}
|
||||||
fetchFn={fetch}
|
fetchFn={fetch}
|
||||||
|
fetchInitialData={fetchInitialData}
|
||||||
handleFavorite={handleFavorite}
|
handleFavorite={handleFavorite}
|
||||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||||
height={height}
|
height={height}
|
||||||
|
|
|
@ -1,38 +1,36 @@
|
||||||
import { MutableRefObject, useCallback, MouseEvent, ChangeEvent, useMemo } from 'react';
|
|
||||||
import { IDatasource } from '@ag-grid-community/core';
|
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { Divider, Flex, Group, Stack } from '@mantine/core';
|
import { Divider, Flex, Group, Stack } from '@mantine/core';
|
||||||
import { openModal } from '@mantine/modals';
|
import { openModal } from '@mantine/modals';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
|
RiAddBoxFill,
|
||||||
|
RiAddCircleFill,
|
||||||
|
RiFilterFill,
|
||||||
RiFolder2Line,
|
RiFolder2Line,
|
||||||
RiMoreFill,
|
RiMoreFill,
|
||||||
RiAddBoxFill,
|
|
||||||
RiPlayFill,
|
RiPlayFill,
|
||||||
RiAddCircleFill,
|
|
||||||
RiRefreshLine,
|
RiRefreshLine,
|
||||||
RiSettings3Fill,
|
RiSettings3Fill,
|
||||||
RiFilterFill,
|
|
||||||
} from 'react-icons/ri';
|
} from 'react-icons/ri';
|
||||||
import { api } from '/@/renderer/api';
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { AlbumListQuery, AlbumListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
import { AlbumListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
||||||
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
|
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
|
||||||
import {
|
|
||||||
AlbumListFilter,
|
|
||||||
useAlbumListStore,
|
|
||||||
useCurrentServer,
|
|
||||||
useListStoreActions,
|
|
||||||
} from '/@/renderer/store';
|
|
||||||
import { ServerType, Play, ListDisplayType, TableColumn } from '/@/renderer/types';
|
|
||||||
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared';
|
|
||||||
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 { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||||
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
|
import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters';
|
||||||
|
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
|
||||||
|
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared';
|
||||||
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
|
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh';
|
||||||
|
import {
|
||||||
|
AlbumListFilter,
|
||||||
|
useCurrentServer,
|
||||||
|
useListStoreActions,
|
||||||
|
useListStoreByKey,
|
||||||
|
} from '/@/renderer/store';
|
||||||
|
import { ListDisplayType, Play, ServerType, TableColumn } from '/@/renderer/types';
|
||||||
|
|
||||||
const FILTERS = {
|
const FILTERS = {
|
||||||
jellyfin: [
|
jellyfin: [
|
||||||
|
@ -77,26 +75,23 @@ const FILTERS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AlbumListHeaderFiltersProps {
|
interface AlbumListHeaderFiltersProps {
|
||||||
customFilters?: Partial<AlbumListFilter>;
|
|
||||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||||
itemCount?: number;
|
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlbumListHeaderFilters = ({
|
export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFiltersProps) => {
|
||||||
customFilters,
|
|
||||||
gridRef,
|
|
||||||
tableRef,
|
|
||||||
itemCount,
|
|
||||||
}: AlbumListHeaderFiltersProps) => {
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { id, pageKey } = useAlbumListContext();
|
const { pageKey, customFilters, handlePlay } = useListContext();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { setFilter, setTablePagination, setTable, setGrid, setDisplayType } =
|
const { setFilter, setTable, setGrid, setDisplayType } = useListStoreActions();
|
||||||
useListStoreActions();
|
const { display, filter, table, grid } = useListStoreByKey({ key: pageKey });
|
||||||
const { display, filter, table, grid } = useAlbumListStore({ id, key: pageKey });
|
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
|
|
||||||
|
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
server,
|
||||||
|
});
|
||||||
|
|
||||||
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id });
|
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id });
|
||||||
|
|
||||||
const sortByLabel =
|
const sortByLabel =
|
||||||
|
@ -107,123 +102,21 @@ export const AlbumListHeaderFilters = ({
|
||||||
|
|
||||||
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER;
|
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER;
|
||||||
|
|
||||||
const fetch = useCallback(
|
const onFilterChange = useCallback(
|
||||||
async (skip: number, take: number, filters: AlbumListFilter) => {
|
(filter: AlbumListFilter) => {
|
||||||
const query: AlbumListQuery = {
|
|
||||||
limit: take,
|
|
||||||
startIndex: skip,
|
|
||||||
...filters,
|
|
||||||
_custom: {
|
|
||||||
jellyfin: {
|
|
||||||
...filters._custom?.jellyfin,
|
|
||||||
...customFilters?._custom?.jellyfin,
|
|
||||||
},
|
|
||||||
navidrome: {
|
|
||||||
...filters._custom?.navidrome,
|
|
||||||
...customFilters?._custom?.navidrome,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...customFilters,
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryKey = queryKeys.albums.list(server?.id || '', query);
|
|
||||||
|
|
||||||
const albums = await queryClient.fetchQuery(
|
|
||||||
queryKey,
|
|
||||||
async ({ signal }) =>
|
|
||||||
api.controller.getAlbumList({
|
|
||||||
apiClientProps: {
|
|
||||||
server,
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
query,
|
|
||||||
}),
|
|
||||||
{ cacheTime: 1000 * 60 * 1 },
|
|
||||||
);
|
|
||||||
|
|
||||||
return albums;
|
|
||||||
},
|
|
||||||
[customFilters, queryClient, server],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterChange = useCallback(
|
|
||||||
async (filters: AlbumListFilter) => {
|
|
||||||
if (isGrid) {
|
if (isGrid) {
|
||||||
gridRef.current?.scrollTo(0);
|
handleRefreshGrid(gridRef, {
|
||||||
gridRef.current?.resetLoadMoreItemsCache();
|
...filter,
|
||||||
|
|
||||||
// Refetching within the virtualized grid may be inconsistent due to it refetching
|
|
||||||
// using an outdated set of filters. To avoid this, we fetch using the updated filters
|
|
||||||
// and then set the grid's data here.
|
|
||||||
const data = await fetch(0, 200, filters);
|
|
||||||
|
|
||||||
if (!data?.items) return;
|
|
||||||
gridRef.current?.setItemData(data.items);
|
|
||||||
} else {
|
|
||||||
const dataSource: IDatasource = {
|
|
||||||
getRows: async (params) => {
|
|
||||||
const limit = params.endRow - params.startRow;
|
|
||||||
const startIndex = params.startRow;
|
|
||||||
|
|
||||||
const query: AlbumListQuery = {
|
|
||||||
limit,
|
|
||||||
startIndex,
|
|
||||||
...filters,
|
|
||||||
...customFilters,
|
...customFilters,
|
||||||
_custom: {
|
});
|
||||||
jellyfin: {
|
|
||||||
...filters._custom?.jellyfin,
|
|
||||||
...customFilters?._custom?.jellyfin,
|
|
||||||
},
|
|
||||||
navidrome: {
|
|
||||||
...filters._custom?.navidrome,
|
|
||||||
...customFilters?._custom?.navidrome,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryKey = queryKeys.albums.list(server?.id || '', query);
|
|
||||||
|
|
||||||
const albumsRes = await queryClient.fetchQuery(
|
|
||||||
queryKey,
|
|
||||||
async ({ signal }) =>
|
|
||||||
api.controller.getAlbumList({
|
|
||||||
apiClientProps: {
|
|
||||||
server,
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
query,
|
|
||||||
}),
|
|
||||||
{ cacheTime: 1000 * 60 * 1 },
|
|
||||||
);
|
|
||||||
|
|
||||||
return params.successCallback(
|
|
||||||
albumsRes?.items || [],
|
|
||||||
albumsRes?.totalRecordCount || 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
rowCount: undefined,
|
|
||||||
};
|
|
||||||
tableRef.current?.api.setDatasource(dataSource);
|
|
||||||
tableRef.current?.api.purgeInfiniteCache();
|
|
||||||
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
|
||||||
|
|
||||||
if (display === ListDisplayType.TABLE_PAGINATED) {
|
|
||||||
setTablePagination({ data: { currentPage: 0 }, key: 'album' });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleRefreshTable(tableRef, {
|
||||||
|
...filter,
|
||||||
|
...customFilters,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
[
|
[customFilters, gridRef, handleRefreshGrid, handleRefreshTable, isGrid, tableRef],
|
||||||
isGrid,
|
|
||||||
gridRef,
|
|
||||||
fetch,
|
|
||||||
tableRef,
|
|
||||||
display,
|
|
||||||
customFilters,
|
|
||||||
server,
|
|
||||||
queryClient,
|
|
||||||
setTablePagination,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenFiltersModal = () => {
|
const handleOpenFiltersModal = () => {
|
||||||
|
@ -232,19 +125,19 @@ export const AlbumListHeaderFilters = ({
|
||||||
<>
|
<>
|
||||||
{server?.type === ServerType.NAVIDROME ? (
|
{server?.type === ServerType.NAVIDROME ? (
|
||||||
<NavidromeAlbumFilters
|
<NavidromeAlbumFilters
|
||||||
|
customFilters={customFilters}
|
||||||
disableArtistFilter={!!customFilters}
|
disableArtistFilter={!!customFilters}
|
||||||
handleFilterChange={handleFilterChange}
|
|
||||||
id={id}
|
|
||||||
pageKey={pageKey}
|
pageKey={pageKey}
|
||||||
serverId={server?.id}
|
serverId={server?.id}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<JellyfinAlbumFilters
|
<JellyfinAlbumFilters
|
||||||
|
customFilters={customFilters}
|
||||||
disableArtistFilter={!!customFilters}
|
disableArtistFilter={!!customFilters}
|
||||||
handleFilterChange={handleFilterChange}
|
|
||||||
id={id}
|
|
||||||
pageKey={pageKey}
|
pageKey={pageKey}
|
||||||
serverId={server?.id}
|
serverId={server?.id}
|
||||||
|
onFilterChange={onFilterChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -255,8 +148,8 @@ export const AlbumListHeaderFilters = ({
|
||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
queryClient.invalidateQueries(queryKeys.albums.list(server?.id || ''));
|
queryClient.invalidateQueries(queryKeys.albums.list(server?.id || ''));
|
||||||
handleFilterChange(filter);
|
onFilterChange(filter);
|
||||||
}, [filter, handleFilterChange, queryClient, server?.id]);
|
}, [filter, onFilterChange, queryClient, server?.id]);
|
||||||
|
|
||||||
const handleSetSortBy = useCallback(
|
const handleSetSortBy = useCallback(
|
||||||
(e: MouseEvent<HTMLButtonElement>) => {
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
|
@ -267,17 +160,18 @@ export const AlbumListHeaderFilters = ({
|
||||||
)?.defaultOrder;
|
)?.defaultOrder;
|
||||||
|
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
sortBy: e.currentTarget.value as AlbumListSort,
|
sortBy: e.currentTarget.value as AlbumListSort,
|
||||||
sortOrder: sortOrder || SortOrder.ASC,
|
sortOrder: sortOrder || SortOrder.ASC,
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: 'album',
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
|
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
},
|
},
|
||||||
[handleFilterChange, server?.type, setFilter],
|
[customFilters, onFilterChange, pageKey, server?.type, setFilter],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSetMusicFolder = useCallback(
|
const handleSetMusicFolder = useCallback(
|
||||||
|
@ -287,86 +181,50 @@ export const AlbumListHeaderFilters = ({
|
||||||
let updatedFilters = null;
|
let updatedFilters = null;
|
||||||
if (e.currentTarget.value === String(filter.musicFolderId)) {
|
if (e.currentTarget.value === String(filter.musicFolderId)) {
|
||||||
updatedFilters = setFilter({
|
updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: { musicFolderId: undefined },
|
data: { musicFolderId: undefined },
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: 'album',
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
} else {
|
} else {
|
||||||
updatedFilters = setFilter({
|
updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: { musicFolderId: e.currentTarget.value },
|
data: { musicFolderId: e.currentTarget.value },
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: 'album',
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
},
|
},
|
||||||
[handleFilterChange, filter.musicFolderId, setFilter],
|
[filter.musicFolderId, onFilterChange, setFilter, customFilters, pageKey],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleToggleSortOrder = useCallback(() => {
|
const handleToggleSortOrder = useCallback(() => {
|
||||||
const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: { sortOrder: newSortOrder },
|
data: { sortOrder: newSortOrder },
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: 'album',
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
}, [filter.sortOrder, handleFilterChange, setFilter]);
|
}, [customFilters, filter.sortOrder, onFilterChange, pageKey, setFilter]);
|
||||||
|
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
|
||||||
|
|
||||||
const handlePlay = async (playType: Play) => {
|
|
||||||
if (!itemCount || itemCount === 0 || !server) return;
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
startIndex: 0,
|
|
||||||
...filter,
|
|
||||||
...customFilters,
|
|
||||||
_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({ apiClientProps: { server, signal }, query }),
|
|
||||||
queryKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const albumIds = albumListRes?.items?.map((a) => a.id) || [];
|
|
||||||
|
|
||||||
handlePlayQueueAdd?.({
|
|
||||||
byItemType: {
|
|
||||||
id: albumIds,
|
|
||||||
type: LibraryItem.ALBUM,
|
|
||||||
},
|
|
||||||
playType,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleItemSize = (e: number) => {
|
const handleItemSize = (e: number) => {
|
||||||
if (isGrid) {
|
if (isGrid) {
|
||||||
setGrid({ data: { itemsPerRow: e }, key: 'album' });
|
setGrid({ data: { itemsPerRow: e }, key: pageKey });
|
||||||
} else {
|
} else {
|
||||||
setTable({ data: { rowHeight: e }, key: 'album' });
|
setTable({ data: { rowHeight: e }, key: pageKey });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetViewType = useCallback(
|
const handleSetViewType = useCallback(
|
||||||
(e: MouseEvent<HTMLButtonElement>) => {
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
if (!e.currentTarget?.value) return;
|
if (!e.currentTarget?.value) return;
|
||||||
setDisplayType({ data: e.currentTarget.value as ListDisplayType, key: 'album' });
|
setDisplayType({ data: e.currentTarget.value as ListDisplayType, key: pageKey });
|
||||||
},
|
},
|
||||||
[setDisplayType],
|
[pageKey, setDisplayType],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTableColumns = (values: TableColumn[]) => {
|
const handleTableColumns = (values: TableColumn[]) => {
|
||||||
|
@ -375,7 +233,7 @@ export const AlbumListHeaderFilters = ({
|
||||||
if (values.length === 0) {
|
if (values.length === 0) {
|
||||||
return setTable({
|
return setTable({
|
||||||
data: { columns: [] },
|
data: { columns: [] },
|
||||||
key: 'album',
|
key: pageKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -383,20 +241,20 @@ export const AlbumListHeaderFilters = ({
|
||||||
if (values.length > existingColumns.length) {
|
if (values.length > existingColumns.length) {
|
||||||
const newColumn = { column: values[values.length - 1], width: 100 };
|
const newColumn = { column: values[values.length - 1], width: 100 };
|
||||||
|
|
||||||
setTable({ data: { columns: [...existingColumns, newColumn] }, key: 'album' });
|
setTable({ data: { columns: [...existingColumns, newColumn] }, key: pageKey });
|
||||||
} else {
|
} else {
|
||||||
// If removing a column
|
// If removing a column
|
||||||
const removed = existingColumns.filter((column) => !values.includes(column.column));
|
const removed = existingColumns.filter((column) => !values.includes(column.column));
|
||||||
const newColumns = existingColumns.filter((column) => !removed.includes(column));
|
const newColumns = existingColumns.filter((column) => !removed.includes(column));
|
||||||
|
|
||||||
setTable({ data: { columns: newColumns }, key: 'album' });
|
setTable({ data: { columns: newColumns }, key: pageKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
return tableRef.current?.api.sizeColumnsToFit();
|
return tableRef.current?.api.sizeColumnsToFit();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAutoFitColumns = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleAutoFitColumns = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setTable({ data: { autoFit: e.currentTarget.checked }, key: 'album' });
|
setTable({ data: { autoFit: e.currentTarget.checked }, key: pageKey });
|
||||||
|
|
||||||
if (e.currentTarget.checked) {
|
if (e.currentTarget.checked) {
|
||||||
tableRef.current?.api.sizeColumnsToFit();
|
tableRef.current?.api.sizeColumnsToFit();
|
||||||
|
@ -511,19 +369,19 @@ export const AlbumListHeaderFilters = ({
|
||||||
<DropdownMenu.Dropdown>
|
<DropdownMenu.Dropdown>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
icon={<RiPlayFill />}
|
icon={<RiPlayFill />}
|
||||||
onClick={() => handlePlay(Play.NOW)}
|
onClick={() => handlePlay?.({ playType: Play.NOW })}
|
||||||
>
|
>
|
||||||
Play
|
Play
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
icon={<RiAddBoxFill />}
|
icon={<RiAddBoxFill />}
|
||||||
onClick={() => handlePlay(Play.LAST)}
|
onClick={() => handlePlay?.({ playType: Play.LAST })}
|
||||||
>
|
>
|
||||||
Add to queue
|
Add to queue
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
icon={<RiAddCircleFill />}
|
icon={<RiAddCircleFill />}
|
||||||
onClick={() => handlePlay(Play.NEXT)}
|
onClick={() => handlePlay?.({ playType: Play.NEXT })}
|
||||||
>
|
>
|
||||||
Add to queue next
|
Add to queue next
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
|
|
@ -1,214 +1,60 @@
|
||||||
import type { ChangeEvent, MutableRefObject } from 'react';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { IDatasource } from '@ag-grid-community/core';
|
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { Flex, Group, Stack } from '@mantine/core';
|
import { Flex, Group, Stack } from '@mantine/core';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { api } from '/@/renderer/api';
|
import type { ChangeEvent, MutableRefObject } from 'react';
|
||||||
import { controller } from '/@/renderer/api/controller';
|
import { useListFilterRefresh } from '../../../hooks/use-list-filter-refresh';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { LibraryItem } from '/@/renderer/api/types';
|
||||||
import { AlbumListQuery, LibraryItem } from '/@/renderer/api/types';
|
|
||||||
import { PageHeader, SearchInput } from '/@/renderer/components';
|
import { PageHeader, SearchInput } from '/@/renderer/components';
|
||||||
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
|
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
|
||||||
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
|
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
import {
|
import {
|
||||||
AlbumListFilter,
|
AlbumListFilter,
|
||||||
useAlbumListFilter,
|
|
||||||
useAlbumListStore,
|
|
||||||
useCurrentServer,
|
useCurrentServer,
|
||||||
useListStoreActions,
|
useListStoreActions,
|
||||||
|
useListStoreByKey,
|
||||||
|
usePlayButtonBehavior,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { ListDisplayType, Play } from '/@/renderer/types';
|
import { ListDisplayType } from '/@/renderer/types';
|
||||||
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
|
|
||||||
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 {
|
interface AlbumListHeaderProps {
|
||||||
customFilters?: Partial<AlbumListFilter>;
|
|
||||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||||
itemCount?: number;
|
itemCount?: number;
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlbumListHeader = ({
|
export const AlbumListHeader = ({ itemCount, gridRef, tableRef, title }: AlbumListHeaderProps) => {
|
||||||
itemCount,
|
|
||||||
gridRef,
|
|
||||||
tableRef,
|
|
||||||
title,
|
|
||||||
customFilters,
|
|
||||||
}: AlbumListHeaderProps) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { setFilter, setTablePagination } = useListStoreActions();
|
const { setFilter, setTablePagination } = useListStoreActions();
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
const { id, pageKey } = useAlbumListContext();
|
const { pageKey, handlePlay } = useListContext();
|
||||||
const { display } = useAlbumListStore({ id, key: pageKey });
|
const { display, filter } = useListStoreByKey({ key: pageKey });
|
||||||
const filter = useAlbumListFilter({ id, key: pageKey });
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
|
||||||
const fetch = useCallback(
|
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
|
||||||
async (skip: number, take: number, filters: AlbumListFilter) => {
|
itemType: LibraryItem.ALBUM,
|
||||||
const query: AlbumListQuery = {
|
|
||||||
limit: take,
|
|
||||||
startIndex: skip,
|
|
||||||
...filters,
|
|
||||||
...customFilters,
|
|
||||||
_custom: {
|
|
||||||
jellyfin: {
|
|
||||||
...filters._custom?.jellyfin,
|
|
||||||
...customFilters?._custom?.jellyfin,
|
|
||||||
},
|
|
||||||
navidrome: {
|
|
||||||
...filters._custom?.navidrome,
|
|
||||||
...customFilters?._custom?.navidrome,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryKey = queryKeys.albums.list(server?.id || '', query);
|
|
||||||
|
|
||||||
const albums = await queryClient.fetchQuery(
|
|
||||||
queryKey,
|
|
||||||
async ({ signal }) =>
|
|
||||||
controller.getAlbumList({
|
|
||||||
apiClientProps: {
|
|
||||||
server,
|
server,
|
||||||
signal,
|
});
|
||||||
},
|
|
||||||
query,
|
|
||||||
}),
|
|
||||||
{ cacheTime: 1000 * 60 * 1 },
|
|
||||||
);
|
|
||||||
|
|
||||||
return albums;
|
|
||||||
},
|
|
||||||
[customFilters, queryClient, server],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleFilterChange = useCallback(
|
|
||||||
async (filters: AlbumListFilter) => {
|
|
||||||
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
|
||||||
const dataSource: IDatasource = {
|
|
||||||
getRows: async (params) => {
|
|
||||||
const limit = params.endRow - params.startRow;
|
|
||||||
const startIndex = params.startRow;
|
|
||||||
|
|
||||||
const query: AlbumListQuery = {
|
|
||||||
limit,
|
|
||||||
startIndex,
|
|
||||||
...filters,
|
|
||||||
...customFilters,
|
|
||||||
_custom: {
|
|
||||||
jellyfin: {
|
|
||||||
...filters._custom?.jellyfin,
|
|
||||||
...customFilters?._custom?.jellyfin,
|
|
||||||
},
|
|
||||||
navidrome: {
|
|
||||||
...filters._custom?.navidrome,
|
|
||||||
...customFilters?._custom?.navidrome,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryKey = queryKeys.albums.list(server?.id || '', query);
|
|
||||||
|
|
||||||
const albumsRes = await queryClient.fetchQuery(
|
|
||||||
queryKey,
|
|
||||||
async ({ signal }) =>
|
|
||||||
api.controller.getAlbumList({
|
|
||||||
apiClientProps: {
|
|
||||||
server,
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
query,
|
|
||||||
}),
|
|
||||||
{ cacheTime: 1000 * 60 * 1 },
|
|
||||||
);
|
|
||||||
|
|
||||||
params.successCallback(
|
|
||||||
albumsRes?.items || [],
|
|
||||||
albumsRes?.totalRecordCount || 0,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
rowCount: undefined,
|
|
||||||
};
|
|
||||||
tableRef.current?.api.setDatasource(dataSource);
|
|
||||||
tableRef.current?.api.purgeInfiniteCache();
|
|
||||||
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
|
||||||
|
|
||||||
if (display === ListDisplayType.TABLE_PAGINATED) {
|
|
||||||
setTablePagination({ data: { currentPage: 0 }, key: 'album' });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
gridRef.current?.scrollTo(0);
|
|
||||||
gridRef.current?.resetLoadMoreItemsCache();
|
|
||||||
|
|
||||||
// Refetching within the virtualized grid may be inconsistent due to it refetching
|
|
||||||
// using an outdated set of filters. To avoid this, we fetch using the updated filters
|
|
||||||
// and then set the grid's data here.
|
|
||||||
const data = await fetch(0, 200, filters);
|
|
||||||
|
|
||||||
if (!data?.items) return;
|
|
||||||
gridRef.current?.setItemData(data.items);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[display, tableRef, customFilters, server, queryClient, setTablePagination, gridRef, fetch],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
|
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const previousSearchTerm = filter.searchTerm;
|
|
||||||
const searchTerm = e.target.value === '' ? undefined : e.target.value;
|
const searchTerm = e.target.value === '' ? undefined : e.target.value;
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
data: { searchTerm },
|
data: { searchTerm },
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: 'album',
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
if (previousSearchTerm !== searchTerm) handleFilterChange(updatedFilters);
|
|
||||||
|
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
|
||||||
|
handleRefreshTable(tableRef, updatedFilters);
|
||||||
|
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
|
||||||
|
} else {
|
||||||
|
handleRefreshGrid(gridRef, updatedFilters);
|
||||||
|
}
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
|
||||||
|
|
||||||
const handlePlay = async (playType: Play) => {
|
|
||||||
if (!itemCount || itemCount === 0) return;
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
startIndex: 0,
|
|
||||||
...filter,
|
|
||||||
...customFilters,
|
|
||||||
_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({ apiClientProps: { server, signal }, query }),
|
|
||||||
queryKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const albumIds = albumListRes?.items?.map((item) => item.id) || [];
|
|
||||||
|
|
||||||
handlePlayQueueAdd?.({
|
|
||||||
byItemType: {
|
|
||||||
id: albumIds,
|
|
||||||
type: LibraryItem.ALBUM,
|
|
||||||
},
|
|
||||||
playType,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
ref={cq.ref}
|
ref={cq.ref}
|
||||||
|
@ -221,7 +67,7 @@ export const AlbumListHeader = ({
|
||||||
>
|
>
|
||||||
<LibraryHeaderBar>
|
<LibraryHeaderBar>
|
||||||
<LibraryHeaderBar.PlayButton
|
<LibraryHeaderBar.PlayButton
|
||||||
onClick={() => handlePlay(playButtonBehavior)}
|
onClick={() => handlePlay?.({ playType: playButtonBehavior })}
|
||||||
/>
|
/>
|
||||||
<LibraryHeaderBar.Title>{title || 'Albums'}</LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title>{title || 'Albums'}</LibraryHeaderBar.Title>
|
||||||
<LibraryHeaderBar.Badge
|
<LibraryHeaderBar.Badge
|
||||||
|
@ -241,9 +87,7 @@ export const AlbumListHeader = ({
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<AlbumListHeaderFilters
|
<AlbumListHeaderFilters
|
||||||
customFilters={customFilters}
|
|
||||||
gridRef={gridRef}
|
gridRef={gridRef}
|
||||||
itemCount={itemCount}
|
|
||||||
tableRef={tableRef}
|
tableRef={tableRef}
|
||||||
/>
|
/>
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
|
|
|
@ -1,68 +1,22 @@
|
||||||
import { useCallback } from 'react';
|
import { useVirtualTable } from '../../../components/virtual-table/hooks/use-virtual-table';
|
||||||
import { api } from '/@/renderer/api';
|
import { LibraryItem } from '/@/renderer/api/types';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
|
||||||
import { AlbumListQuery, AlbumListResponse, LibraryItem } from '/@/renderer/api/types';
|
|
||||||
import { VirtualTable } from '/@/renderer/components/virtual-table';
|
|
||||||
import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context';
|
|
||||||
import {
|
|
||||||
useCurrentServer,
|
|
||||||
useAlbumListFilter,
|
|
||||||
useListStoreActions,
|
|
||||||
useAlbumListStore,
|
|
||||||
} from '/@/renderer/store';
|
|
||||||
import { ALBUM_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
|
||||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
||||||
import {
|
import { VirtualTable } from '/@/renderer/components/virtual-table';
|
||||||
useVirtualTable,
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
AgGridFetchFn,
|
import { ALBUM_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
||||||
} from '../../../components/virtual-table/hooks/use-virtual-table';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
|
||||||
export const AlbumListTableView = ({ tableRef, itemCount }: any) => {
|
export const AlbumListTableView = ({ tableRef, itemCount }: any) => {
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { id, pageKey } = useAlbumListContext();
|
const { pageKey, customFilters } = useListContext();
|
||||||
const filter = useAlbumListFilter({ id, key: pageKey });
|
|
||||||
const { setTable, setTablePagination } = useListStoreActions();
|
|
||||||
const listProperties = useAlbumListStore({ id, key: pageKey });
|
|
||||||
|
|
||||||
const fetchFn: AgGridFetchFn<
|
const tableProps = useVirtualTable({
|
||||||
AlbumListResponse,
|
|
||||||
Omit<AlbumListQuery, 'startIndex'>
|
|
||||||
> = useCallback(
|
|
||||||
async ({ filter, limit, startIndex }, signal) => {
|
|
||||||
const res = api.controller.getAlbumList({
|
|
||||||
apiClientProps: {
|
|
||||||
server,
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
...filter,
|
|
||||||
limit,
|
|
||||||
sortBy: filter.sortBy,
|
|
||||||
sortOrder: filter.sortOrder,
|
|
||||||
startIndex,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return res;
|
|
||||||
},
|
|
||||||
[server],
|
|
||||||
);
|
|
||||||
|
|
||||||
const tableProps = useVirtualTable<AlbumListResponse, Omit<AlbumListQuery, 'startIndex'>>({
|
|
||||||
contextMenu: ALBUM_CONTEXT_MENU_ITEMS,
|
contextMenu: ALBUM_CONTEXT_MENU_ITEMS,
|
||||||
fetch: {
|
customFilters,
|
||||||
filter,
|
|
||||||
fn: fetchFn,
|
|
||||||
itemCount,
|
|
||||||
queryKey: queryKeys.albums.list,
|
|
||||||
server,
|
|
||||||
},
|
|
||||||
itemCount,
|
itemCount,
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
pageKey,
|
pageKey,
|
||||||
properties: listProperties,
|
server,
|
||||||
setTable,
|
|
||||||
setTablePagination,
|
|
||||||
tableRef,
|
tableRef,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -71,7 +25,7 @@ export const AlbumListTableView = ({ tableRef, itemCount }: any) => {
|
||||||
<VirtualTable
|
<VirtualTable
|
||||||
// https://github.com/ag-grid/ag-grid/issues/5284
|
// https://github.com/ag-grid/ag-grid/issues/5284
|
||||||
// Key is used to force remount of table when display, rowHeight, or server changes
|
// Key is used to force remount of table when display, rowHeight, or server changes
|
||||||
key={`table-${listProperties.display}-${listProperties.table.rowHeight}-${server?.id}`}
|
key={`table-${tableProps.rowHeight}-${server?.id}`}
|
||||||
ref={tableRef}
|
ref={tableRef}
|
||||||
{...tableProps}
|
{...tableProps}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,28 +1,29 @@
|
||||||
import { ChangeEvent, useMemo, useState } from 'react';
|
|
||||||
import { Divider, Group, Stack } from '@mantine/core';
|
import { Divider, Group, Stack } from '@mantine/core';
|
||||||
import { MultiSelect, NumberInput, SpinnerIcon, Switch, Text } from '/@/renderer/components';
|
|
||||||
import { AlbumListFilter, useAlbumListFilter, useListStoreActions } from '/@/renderer/store';
|
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { useGenreList } from '/@/renderer/features/genres';
|
import { ChangeEvent, useMemo, useState } from 'react';
|
||||||
|
import { useListFilterByKey } from '../../../store/list.store';
|
||||||
import { AlbumArtistListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
import { AlbumArtistListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
||||||
|
import { MultiSelect, NumberInput, SpinnerIcon, Switch, Text } from '/@/renderer/components';
|
||||||
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
|
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
|
||||||
|
import { useGenreList } from '/@/renderer/features/genres';
|
||||||
|
import { AlbumListFilter, useListStoreActions } from '/@/renderer/store';
|
||||||
|
|
||||||
interface JellyfinAlbumFiltersProps {
|
interface JellyfinAlbumFiltersProps {
|
||||||
|
customFilters?: Partial<AlbumListFilter>;
|
||||||
disableArtistFilter?: boolean;
|
disableArtistFilter?: boolean;
|
||||||
handleFilterChange: (filters: AlbumListFilter) => void;
|
onFilterChange: (filters: AlbumListFilter) => void;
|
||||||
id?: string;
|
|
||||||
pageKey: string;
|
pageKey: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JellyfinAlbumFilters = ({
|
export const JellyfinAlbumFilters = ({
|
||||||
|
customFilters,
|
||||||
disableArtistFilter,
|
disableArtistFilter,
|
||||||
handleFilterChange,
|
onFilterChange,
|
||||||
pageKey,
|
pageKey,
|
||||||
id,
|
|
||||||
serverId,
|
serverId,
|
||||||
}: JellyfinAlbumFiltersProps) => {
|
}: JellyfinAlbumFiltersProps) => {
|
||||||
const filter = useAlbumListFilter({ id, key: pageKey });
|
const filter = useListFilterByKey({ key: pageKey });
|
||||||
const { setFilter } = useListStoreActions();
|
const { setFilter } = useListStoreActions();
|
||||||
|
|
||||||
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
|
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
|
||||||
|
@ -45,6 +46,7 @@ export const JellyfinAlbumFilters = ({
|
||||||
label: 'Is favorited',
|
label: 'Is favorited',
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: {
|
||||||
...filter._custom,
|
...filter._custom,
|
||||||
|
@ -57,7 +59,7 @@ export const JellyfinAlbumFilters = ({
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
},
|
},
|
||||||
value: filter._custom?.jellyfin?.IsFavorite,
|
value: filter._custom?.jellyfin?.IsFavorite,
|
||||||
},
|
},
|
||||||
|
@ -66,6 +68,7 @@ export const JellyfinAlbumFilters = ({
|
||||||
const handleMinYearFilter = debounce((e: number | string) => {
|
const handleMinYearFilter = debounce((e: number | string) => {
|
||||||
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
|
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: {
|
||||||
...filter._custom,
|
...filter._custom,
|
||||||
|
@ -78,12 +81,13 @@ export const JellyfinAlbumFilters = ({
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
const handleMaxYearFilter = debounce((e: number | string) => {
|
const handleMaxYearFilter = debounce((e: number | string) => {
|
||||||
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
|
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: {
|
||||||
...filter._custom,
|
...filter._custom,
|
||||||
|
@ -96,12 +100,13 @@ export const JellyfinAlbumFilters = ({
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
const handleGenresFilter = debounce((e: string[] | undefined) => {
|
const handleGenresFilter = debounce((e: string[] | undefined) => {
|
||||||
const genreFilterString = e?.length ? e.join(',') : undefined;
|
const genreFilterString = e?.length ? e.join(',') : undefined;
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: {
|
||||||
...filter._custom,
|
...filter._custom,
|
||||||
|
@ -114,7 +119,7 @@ export const JellyfinAlbumFilters = ({
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
|
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
|
||||||
|
@ -144,6 +149,7 @@ export const JellyfinAlbumFilters = ({
|
||||||
const handleAlbumArtistFilter = (e: string[] | null) => {
|
const handleAlbumArtistFilter = (e: string[] | null) => {
|
||||||
const albumArtistFilterString = e?.length ? e.join(',') : undefined;
|
const albumArtistFilterString = e?.length ? e.join(',') : undefined;
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: {
|
||||||
...filter._custom,
|
...filter._custom,
|
||||||
|
@ -156,7 +162,7 @@ export const JellyfinAlbumFilters = ({
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
import { ChangeEvent, useMemo, useState } from 'react';
|
import { ChangeEvent, useMemo, useState } from 'react';
|
||||||
import { Divider, Group, Stack } from '@mantine/core';
|
import { Divider, Group, Stack } from '@mantine/core';
|
||||||
import { NumberInput, Switch, Text, Select, SpinnerIcon } from '/@/renderer/components';
|
import { NumberInput, Switch, Text, Select, SpinnerIcon } from '/@/renderer/components';
|
||||||
import { AlbumListFilter, useAlbumListFilter, useListStoreActions } from '/@/renderer/store';
|
import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { useGenreList } from '/@/renderer/features/genres';
|
import { useGenreList } from '/@/renderer/features/genres';
|
||||||
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
|
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
|
||||||
import { AlbumArtistListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
import { AlbumArtistListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
|
||||||
|
|
||||||
interface NavidromeAlbumFiltersProps {
|
interface NavidromeAlbumFiltersProps {
|
||||||
|
customFilters?: Partial<AlbumListFilter>;
|
||||||
disableArtistFilter?: boolean;
|
disableArtistFilter?: boolean;
|
||||||
handleFilterChange: (filters: AlbumListFilter) => void;
|
onFilterChange: (filters: AlbumListFilter) => void;
|
||||||
id?: string;
|
|
||||||
pageKey: string;
|
pageKey: string;
|
||||||
serverId?: string;
|
serverId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NavidromeAlbumFilters = ({
|
export const NavidromeAlbumFilters = ({
|
||||||
handleFilterChange,
|
customFilters,
|
||||||
|
onFilterChange,
|
||||||
disableArtistFilter,
|
disableArtistFilter,
|
||||||
pageKey,
|
pageKey,
|
||||||
id,
|
|
||||||
serverId,
|
serverId,
|
||||||
}: NavidromeAlbumFiltersProps) => {
|
}: NavidromeAlbumFiltersProps) => {
|
||||||
const filter = useAlbumListFilter({ id, key: pageKey });
|
const { filter } = useListStoreByKey({ key: pageKey });
|
||||||
const { setFilter } = useListStoreActions();
|
const { setFilter } = useListStoreActions();
|
||||||
|
|
||||||
const genreListQuery = useGenreList({ query: null, serverId });
|
const genreListQuery = useGenreList({ query: null, serverId });
|
||||||
|
@ -37,6 +37,7 @@ export const NavidromeAlbumFilters = ({
|
||||||
|
|
||||||
const handleGenresFilter = debounce((e: string | null) => {
|
const handleGenresFilter = debounce((e: string | null) => {
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: {
|
||||||
...filter._custom,
|
...filter._custom,
|
||||||
|
@ -47,9 +48,9 @@ export const NavidromeAlbumFilters = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: 'album',
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
const toggleFilters = [
|
const toggleFilters = [
|
||||||
|
@ -57,6 +58,7 @@ export const NavidromeAlbumFilters = ({
|
||||||
label: 'Is rated',
|
label: 'Is rated',
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: {
|
||||||
...filter._custom,
|
...filter._custom,
|
||||||
|
@ -69,7 +71,7 @@ export const NavidromeAlbumFilters = ({
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
},
|
},
|
||||||
value: filter._custom?.navidrome?.has_rating,
|
value: filter._custom?.navidrome?.has_rating,
|
||||||
},
|
},
|
||||||
|
@ -77,6 +79,7 @@ export const NavidromeAlbumFilters = ({
|
||||||
label: 'Is favorited',
|
label: 'Is favorited',
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: {
|
||||||
...filter._custom,
|
...filter._custom,
|
||||||
|
@ -89,7 +92,7 @@ export const NavidromeAlbumFilters = ({
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
},
|
},
|
||||||
value: filter._custom?.navidrome?.starred,
|
value: filter._custom?.navidrome?.starred,
|
||||||
},
|
},
|
||||||
|
@ -97,6 +100,7 @@ export const NavidromeAlbumFilters = ({
|
||||||
label: 'Is compilation',
|
label: 'Is compilation',
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: {
|
||||||
...filter._custom,
|
...filter._custom,
|
||||||
|
@ -109,7 +113,7 @@ export const NavidromeAlbumFilters = ({
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
},
|
},
|
||||||
value: filter._custom?.navidrome?.compilation,
|
value: filter._custom?.navidrome?.compilation,
|
||||||
},
|
},
|
||||||
|
@ -117,6 +121,7 @@ export const NavidromeAlbumFilters = ({
|
||||||
label: 'Is recently played',
|
label: 'Is recently played',
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: {
|
||||||
...filter._custom,
|
...filter._custom,
|
||||||
|
@ -129,7 +134,7 @@ export const NavidromeAlbumFilters = ({
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
},
|
},
|
||||||
value: filter._custom?.navidrome?.recently_played,
|
value: filter._custom?.navidrome?.recently_played,
|
||||||
},
|
},
|
||||||
|
@ -137,6 +142,7 @@ export const NavidromeAlbumFilters = ({
|
||||||
|
|
||||||
const handleYearFilter = debounce((e: number | string) => {
|
const handleYearFilter = debounce((e: number | string) => {
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters = setFilter({
|
||||||
|
customFilters,
|
||||||
data: {
|
data: {
|
||||||
_custom: {
|
_custom: {
|
||||||
navidrome: {
|
navidrome: {
|
||||||
|
@ -149,7 +155,7 @@ export const NavidromeAlbumFilters = ({
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
|
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
|
||||||
|
@ -191,7 +197,7 @@ export const NavidromeAlbumFilters = ({
|
||||||
itemType: LibraryItem.ALBUM,
|
itemType: LibraryItem.ALBUM,
|
||||||
key: pageKey,
|
key: pageKey,
|
||||||
}) as AlbumListFilter;
|
}) as AlbumListFilter;
|
||||||
handleFilterChange(updatedFilters);
|
onFilterChange(updatedFilters);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,28 +1,39 @@
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
|
||||||
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';
|
|
||||||
import { useRef } from 'react';
|
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
import { generatePageKey, useAlbumListFilter, useCurrentServer } from '/@/renderer/store';
|
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { AlbumListContext } from '/@/renderer/features/albums/context/album-list-context';
|
import { api } from '/@/renderer/api';
|
||||||
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
|
import { LibraryItem } from '/@/renderer/api/types';
|
||||||
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
|
import { ListContext } from '/@/renderer/context/list-context';
|
||||||
|
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
|
||||||
|
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
|
||||||
|
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
|
||||||
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||||
|
import { AnimatedPage } from '/@/renderer/features/shared';
|
||||||
|
import { queryClient } from '/@/renderer/lib/react-query';
|
||||||
|
import { useCurrentServer, useListFilterByKey } from '/@/renderer/store';
|
||||||
|
import { Play } from '/@/renderer/types';
|
||||||
|
|
||||||
const AlbumListRoute = () => {
|
const AlbumListRoute = () => {
|
||||||
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
const tableRef = useRef<AgGridReactType | null>(null);
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const { albumArtistId } = useParams();
|
const { albumArtistId } = useParams();
|
||||||
|
const pageKey = albumArtistId ? `albumArtistAlbum` : 'album';
|
||||||
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||||
|
|
||||||
const pageKey = generatePageKey(
|
const customFilters = useMemo(() => {
|
||||||
'album',
|
return {
|
||||||
albumArtistId ? `${albumArtistId}_${server?.id}` : undefined,
|
...(albumArtistId && { artistIds: [albumArtistId] }),
|
||||||
);
|
};
|
||||||
|
}, [albumArtistId]);
|
||||||
|
|
||||||
const albumListFilter = useAlbumListFilter({ id: albumArtistId || undefined, key: pageKey });
|
const albumListFilter = useListFilterByKey({
|
||||||
|
filter: customFilters,
|
||||||
|
key: pageKey,
|
||||||
|
});
|
||||||
|
|
||||||
const itemCountCheck = useAlbumList({
|
const itemCountCheck = useAlbumList({
|
||||||
options: {
|
options: {
|
||||||
|
@ -42,9 +53,43 @@ const AlbumListRoute = () => {
|
||||||
? undefined
|
? undefined
|
||||||
: itemCountCheck.data?.totalRecordCount;
|
: itemCountCheck.data?.totalRecordCount;
|
||||||
|
|
||||||
|
const handlePlay = useCallback(
|
||||||
|
async (args: { initialSongId?: string; playType: Play }) => {
|
||||||
|
if (!itemCount || itemCount === 0) return;
|
||||||
|
const { playType } = args;
|
||||||
|
const query = {
|
||||||
|
startIndex: 0,
|
||||||
|
...albumListFilter,
|
||||||
|
...customFilters,
|
||||||
|
};
|
||||||
|
const queryKey = queryKeys.albums.list(server?.id || '', query);
|
||||||
|
|
||||||
|
const albumListRes = await queryClient.fetchQuery({
|
||||||
|
queryFn: ({ signal }) => {
|
||||||
|
return api.controller.getAlbumList({
|
||||||
|
apiClientProps: { server, signal },
|
||||||
|
query,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
queryKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const albumIds = albumListRes?.items?.map((a) => a.id) || [];
|
||||||
|
|
||||||
|
handlePlayQueueAdd?.({
|
||||||
|
byItemType: {
|
||||||
|
id: albumIds,
|
||||||
|
type: LibraryItem.ALBUM,
|
||||||
|
},
|
||||||
|
playType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[albumListFilter, customFilters, handlePlayQueueAdd, itemCount, server],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
<AlbumListContext.Provider value={{ id: albumArtistId || undefined, pageKey }}>
|
<ListContext.Provider value={{ customFilters, handlePlay, id: albumArtistId, pageKey }}>
|
||||||
<AlbumListHeader
|
<AlbumListHeader
|
||||||
gridRef={gridRef}
|
gridRef={gridRef}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
|
@ -56,7 +101,7 @@ const AlbumListRoute = () => {
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
tableRef={tableRef}
|
tableRef={tableRef}
|
||||||
/>
|
/>
|
||||||
</AlbumListContext.Provider>
|
</ListContext.Provider>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -55,6 +55,7 @@ export interface ListState {
|
||||||
item: {
|
item: {
|
||||||
album: ListItemProps<AlbumListFilter>;
|
album: ListItemProps<AlbumListFilter>;
|
||||||
albumArtist: ListItemProps<AlbumArtistListFilter>;
|
albumArtist: ListItemProps<AlbumArtistListFilter>;
|
||||||
|
albumArtistAlbum: ListItemProps<AlbumListFilter>;
|
||||||
albumArtistSong: ListItemProps<SongListFilter>;
|
albumArtistSong: ListItemProps<SongListFilter>;
|
||||||
albumDetail: ListItemProps<any>;
|
albumDetail: ListItemProps<any>;
|
||||||
playlist: ListItemProps<PlaylistListFilter>;
|
playlist: ListItemProps<PlaylistListFilter>;
|
||||||
|
@ -380,6 +381,47 @@ export const useListStore = create<ListSlice>()(
|
||||||
scrollOffset: 0,
|
scrollOffset: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
albumArtistAlbum: {
|
||||||
|
display: ListDisplayType.POSTER,
|
||||||
|
filter: {
|
||||||
|
sortBy: AlbumListSort.RECENTLY_ADDED,
|
||||||
|
sortOrder: SortOrder.DESC,
|
||||||
|
},
|
||||||
|
grid: { itemsPerRow: 5, scrollOffset: 0 },
|
||||||
|
table: {
|
||||||
|
autoFit: true,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
column: TableColumn.ROW_INDEX,
|
||||||
|
width: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: TableColumn.TITLE_COMBINED,
|
||||||
|
width: 500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: TableColumn.DURATION,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: TableColumn.ALBUM_ARTIST,
|
||||||
|
width: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: TableColumn.YEAR,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
currentPage: 1,
|
||||||
|
itemsPerPage: 100,
|
||||||
|
totalItems: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
},
|
||||||
|
rowHeight: 60,
|
||||||
|
scrollOffset: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
albumArtistSong: {
|
albumArtistSong: {
|
||||||
display: ListDisplayType.TABLE,
|
display: ListDisplayType.TABLE,
|
||||||
filter: {
|
filter: {
|
||||||
|
@ -553,69 +595,6 @@ export const useListFilterByKey = <TFilter>(args: { filter?: Partial<TFilter>; k
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAlbumListStore = (args?: { id?: string; key?: string }) =>
|
|
||||||
useListStore((state) => {
|
|
||||||
const detail = args?.key ? state.detail[args.key] : undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state.item.album,
|
|
||||||
filter: {
|
|
||||||
...state.item.album.filter,
|
|
||||||
...detail?.filter,
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
...state.item.album.grid,
|
|
||||||
...detail?.grid,
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
...state.item.album.table,
|
|
||||||
...detail?.table,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, shallow);
|
|
||||||
|
|
||||||
export const useSongListStore = (args?: { id?: string; key?: string }) =>
|
|
||||||
useListStore((state) => {
|
|
||||||
const detail = args?.key ? state.detail[args.key] : undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state.item.song,
|
|
||||||
filter: {
|
|
||||||
...state.item.song.filter,
|
|
||||||
...detail?.filter,
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
...state.item.song.grid,
|
|
||||||
...detail?.grid,
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
...state.item.song.table,
|
|
||||||
...detail?.table,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, shallow);
|
|
||||||
|
|
||||||
export const usePlaylistListStore = (args?: { key?: string }) =>
|
|
||||||
useListStore((state) => {
|
|
||||||
const detail = args?.key ? state.detail[args.key] : undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state.item.playlist,
|
|
||||||
filter: {
|
|
||||||
...state.item.playlist.filter,
|
|
||||||
...detail?.filter,
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
...state.item.playlist.grid,
|
|
||||||
...detail?.grid,
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
...state.item.playlist.table,
|
|
||||||
...detail?.table,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}, shallow);
|
|
||||||
|
|
||||||
export const useAlbumListFilter = (args: { id?: string; key?: string }) =>
|
export const useAlbumListFilter = (args: { id?: string; key?: string }) =>
|
||||||
useListStore((state) => {
|
useListStore((state) => {
|
||||||
return state._actions.getFilter({
|
return state._actions.getFilter({
|
||||||
|
|
Reference in a new issue