mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
implement new lists for albums
This commit is contained in:
@@ -1,42 +1,128 @@
|
|||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import { lazy, Suspense } from 'react';
|
||||||
|
|
||||||
import { lazy, MutableRefObject, Suspense } from 'react';
|
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
||||||
|
import { ItemListSettings, useCurrentServer, useListSettings } from '/@/renderer/store';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
|
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
|
||||||
import { useListStoreByKey } from '/@/renderer/store';
|
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import { ListDisplayType } from '/@/shared/types/types';
|
import { ItemListKey, ListDisplayType, ListPaginationType } from '/@/shared/types/types';
|
||||||
|
|
||||||
const AlbumListGridView = lazy(() =>
|
const AlbumListInfiniteGrid = lazy(() =>
|
||||||
import('/@/renderer/features/albums/components/album-list-grid-view').then((module) => ({
|
import('/@/renderer/features/albums/components/album-list-infinite-grid').then((module) => ({
|
||||||
default: module.AlbumListGridView,
|
default: module.AlbumListInfiniteGrid,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const AlbumListTableView = lazy(() =>
|
const AlbumListPaginatedGrid = lazy(() =>
|
||||||
import('/@/renderer/features/albums/components/album-list-table-view').then((module) => ({
|
import('/@/renderer/features/albums/components/album-list-paginated-grid').then((module) => ({
|
||||||
default: module.AlbumListTableView,
|
default: module.AlbumListPaginatedGrid,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
interface AlbumListContentProps {
|
const AlbumListInfiniteTable = lazy(() =>
|
||||||
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
|
import('/@/renderer/features/albums/components/album-list-infinite-table').then((module) => ({
|
||||||
itemCount?: number;
|
default: module.AlbumListInfiniteTable,
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
})),
|
||||||
}
|
);
|
||||||
|
|
||||||
export const AlbumListContent = ({ gridRef, itemCount, tableRef }: AlbumListContentProps) => {
|
const AlbumListPaginatedTable = lazy(() =>
|
||||||
const { pageKey } = useListContext();
|
import('/@/renderer/features/albums/components/album-list-paginated-table').then((module) => ({
|
||||||
const { display } = useListStoreByKey({ key: pageKey });
|
default: module.AlbumListPaginatedTable,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const AlbumListContent = () => {
|
||||||
|
const { display, grid, itemsPerPage, pagination, table } = useListSettings(ItemListKey.ALBUM);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Spinner container />}>
|
<Suspense fallback={<Spinner container />}>
|
||||||
{display === ListDisplayType.CARD || display === ListDisplayType.GRID ? (
|
<AlbumListView
|
||||||
<AlbumListGridView gridRef={gridRef} itemCount={itemCount} />
|
display={display}
|
||||||
) : (
|
grid={grid}
|
||||||
<AlbumListTableView itemCount={itemCount} tableRef={tableRef} />
|
itemsPerPage={itemsPerPage}
|
||||||
)}
|
pagination={pagination}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const AlbumListView = ({
|
||||||
|
display,
|
||||||
|
grid,
|
||||||
|
itemsPerPage,
|
||||||
|
pagination,
|
||||||
|
table,
|
||||||
|
}: ItemListSettings) => {
|
||||||
|
const server = useCurrentServer();
|
||||||
|
|
||||||
|
const filters = useAlbumListFilters();
|
||||||
|
const query = filters.query;
|
||||||
|
|
||||||
|
switch (display) {
|
||||||
|
case ListDisplayType.GRID: {
|
||||||
|
switch (pagination) {
|
||||||
|
case ListPaginationType.INFINITE: {
|
||||||
|
return (
|
||||||
|
<AlbumListInfiniteGrid
|
||||||
|
gap={grid.itemGap}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||||
|
query={query}
|
||||||
|
serverId={server.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case ListPaginationType.PAGINATED: {
|
||||||
|
return (
|
||||||
|
<AlbumListPaginatedGrid
|
||||||
|
gap={grid.itemGap}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||||
|
query={query}
|
||||||
|
serverId={server.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ListDisplayType.TABLE: {
|
||||||
|
switch (pagination) {
|
||||||
|
case ListPaginationType.INFINITE: {
|
||||||
|
return (
|
||||||
|
<AlbumListInfiniteTable
|
||||||
|
columns={table.columns}
|
||||||
|
enableAlternateRowColors={table.enableAlternateRowColors}
|
||||||
|
enableHorizontalBorders={table.enableHorizontalBorders}
|
||||||
|
enableRowHoverHighlight={table.enableRowHoverHighlight}
|
||||||
|
enableVerticalBorders={table.enableVerticalBorders}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
query={query}
|
||||||
|
serverId={server.id}
|
||||||
|
size={table.size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case ListPaginationType.PAGINATED: {
|
||||||
|
return (
|
||||||
|
<AlbumListPaginatedTable
|
||||||
|
columns={table.columns}
|
||||||
|
enableAlternateRowColors={table.enableAlternateRowColors}
|
||||||
|
enableHorizontalBorders={table.enableHorizontalBorders}
|
||||||
|
enableRowHoverHighlight={table.enableRowHoverHighlight}
|
||||||
|
enableVerticalBorders={table.enableVerticalBorders}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
query={query}
|
||||||
|
serverId={server.id}
|
||||||
|
size={table.size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,225 +0,0 @@
|
|||||||
import { QueryKey, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
import { useSearchParams } from 'react-router-dom';
|
|
||||||
import AutoSizer, { Size } from 'react-virtualized-auto-sizer';
|
|
||||||
import { ListOnScrollProps } from 'react-window';
|
|
||||||
|
|
||||||
import { controller } from '/@/renderer/api/controller';
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
|
||||||
import { ALBUM_CARD_ROWS } from '/@/renderer/components/card/card-rows';
|
|
||||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
|
|
||||||
import { VirtualInfiniteGrid } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
|
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
|
||||||
import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite';
|
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
|
||||||
import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
|
|
||||||
import {
|
|
||||||
Album,
|
|
||||||
AlbumListQuery,
|
|
||||||
AlbumListResponse,
|
|
||||||
AlbumListSort,
|
|
||||||
LibraryItem,
|
|
||||||
} from '/@/shared/types/domain-types';
|
|
||||||
import { CardRow, ListDisplayType } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
export const AlbumListGridView = ({ gridRef, itemCount }: any) => {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const server = useCurrentServer();
|
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
|
||||||
const { customFilters, id, pageKey } = useListContext();
|
|
||||||
const { display, filter, grid } = useListStoreByKey<AlbumListQuery>({ key: pageKey });
|
|
||||||
const { setGrid } = useListStoreActions();
|
|
||||||
|
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
|
||||||
const scrollOffset = searchParams.get('scrollOffset');
|
|
||||||
const initialScrollOffset = Number(id ? scrollOffset : grid?.scrollOffset) || 0;
|
|
||||||
|
|
||||||
const handleFavorite = useHandleFavorite({ gridRef });
|
|
||||||
|
|
||||||
const cardRows = useMemo(() => {
|
|
||||||
const rows: CardRow<Album>[] = [ALBUM_CARD_ROWS.name];
|
|
||||||
|
|
||||||
switch (filter.sortBy) {
|
|
||||||
case AlbumListSort.ALBUM_ARTIST:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.albumArtists);
|
|
||||||
rows.push(ALBUM_CARD_ROWS.releaseYear);
|
|
||||||
break;
|
|
||||||
case AlbumListSort.ARTIST:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.artists);
|
|
||||||
rows.push(ALBUM_CARD_ROWS.releaseYear);
|
|
||||||
break;
|
|
||||||
case AlbumListSort.COMMUNITY_RATING:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.albumArtists);
|
|
||||||
break;
|
|
||||||
case AlbumListSort.DURATION:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.albumArtists);
|
|
||||||
rows.push(ALBUM_CARD_ROWS.duration);
|
|
||||||
break;
|
|
||||||
case AlbumListSort.EXPLICIT_STATUS:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.albumArtists);
|
|
||||||
rows.push(ALBUM_CARD_ROWS.explicitStatus);
|
|
||||||
break;
|
|
||||||
case AlbumListSort.FAVORITED:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.albumArtists);
|
|
||||||
rows.push(ALBUM_CARD_ROWS.releaseYear);
|
|
||||||
break;
|
|
||||||
case AlbumListSort.NAME:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.albumArtists);
|
|
||||||
rows.push(ALBUM_CARD_ROWS.releaseYear);
|
|
||||||
break;
|
|
||||||
case AlbumListSort.PLAY_COUNT:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.albumArtists);
|
|
||||||
rows.push(ALBUM_CARD_ROWS.playCount);
|
|
||||||
break;
|
|
||||||
case AlbumListSort.RANDOM:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.albumArtists);
|
|
||||||
rows.push(ALBUM_CARD_ROWS.releaseYear);
|
|
||||||
break;
|
|
||||||
case AlbumListSort.RATING:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.albumArtists);
|
|
||||||
rows.push(ALBUM_CARD_ROWS.rating);
|
|
||||||
break;
|
|
||||||
case AlbumListSort.RECENTLY_ADDED:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.albumArtists);
|
|
||||||
rows.push(ALBUM_CARD_ROWS.createdAt);
|
|
||||||
break;
|
|
||||||
case AlbumListSort.RECENTLY_PLAYED:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.albumArtists);
|
|
||||||
rows.push(ALBUM_CARD_ROWS.lastPlayedAt);
|
|
||||||
break;
|
|
||||||
case AlbumListSort.SONG_COUNT:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.albumArtists);
|
|
||||||
rows.push(ALBUM_CARD_ROWS.songCount);
|
|
||||||
break;
|
|
||||||
case AlbumListSort.YEAR:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.albumArtists);
|
|
||||||
rows.push(ALBUM_CARD_ROWS.releaseYear);
|
|
||||||
break;
|
|
||||||
case AlbumListSort.RELEASE_DATE:
|
|
||||||
rows.push(ALBUM_CARD_ROWS.albumArtists);
|
|
||||||
rows.push(ALBUM_CARD_ROWS.releaseDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
return rows;
|
|
||||||
}, [filter.sortBy]);
|
|
||||||
|
|
||||||
const handleGridScroll = useCallback(
|
|
||||||
(e: ListOnScrollProps) => {
|
|
||||||
if (id) {
|
|
||||||
setSearchParams(
|
|
||||||
(params) => {
|
|
||||||
params.set('scrollOffset', String(e.scrollOffset));
|
|
||||||
return params;
|
|
||||||
},
|
|
||||||
{ replace: true },
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[id, pageKey, setGrid, setSearchParams],
|
|
||||||
);
|
|
||||||
|
|
||||||
const fetchInitialData = useCallback(() => {
|
|
||||||
const query: AlbumListQuery = {
|
|
||||||
...filter,
|
|
||||||
...customFilters,
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryKey = queryKeys.albums.list(server?.id || '', query, id);
|
|
||||||
|
|
||||||
const queriesFromCache: [QueryKey, AlbumListResponse | undefined][] =
|
|
||||||
queryClient.getQueriesData({
|
|
||||||
exact: false,
|
|
||||||
fetchStatus: 'idle',
|
|
||||||
queryKey,
|
|
||||||
stale: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const itemData: Album[] = [];
|
|
||||||
|
|
||||||
for (const [, data] of queriesFromCache) {
|
|
||||||
const { items, startIndex } = data || {};
|
|
||||||
|
|
||||||
if (items && items.length !== 1 && 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, id, queryClient, server?.id]);
|
|
||||||
|
|
||||||
const fetch = useCallback(
|
|
||||||
async ({ skip, take }: { skip: number; take: number }) => {
|
|
||||||
if (!server) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const query: AlbumListQuery = {
|
|
||||||
limit: take,
|
|
||||||
...filter,
|
|
||||||
...customFilters,
|
|
||||||
startIndex: skip,
|
|
||||||
};
|
|
||||||
|
|
||||||
const queryKey = queryKeys.albums.list(server?.id || '', query, id);
|
|
||||||
|
|
||||||
const albums = await queryClient.fetchQuery({
|
|
||||||
queryFn: async ({ signal }) =>
|
|
||||||
controller.getAlbumList({
|
|
||||||
apiClientProps: {
|
|
||||||
serverId: server?.id || '',
|
|
||||||
signal,
|
|
||||||
},
|
|
||||||
query,
|
|
||||||
}),
|
|
||||||
queryKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
return albums;
|
|
||||||
},
|
|
||||||
[customFilters, filter, id, queryClient, server],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualGridAutoSizerContainer>
|
|
||||||
<AutoSizer>
|
|
||||||
{({ height, width }: Size) => (
|
|
||||||
<VirtualInfiniteGrid
|
|
||||||
cardRows={cardRows}
|
|
||||||
display={display || ListDisplayType.CARD}
|
|
||||||
fetchFn={fetch}
|
|
||||||
fetchInitialData={fetchInitialData}
|
|
||||||
handleFavorite={handleFavorite}
|
|
||||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
|
||||||
height={height}
|
|
||||||
initialScrollOffset={initialScrollOffset}
|
|
||||||
itemCount={itemCount || 0}
|
|
||||||
itemGap={grid?.itemGap ?? 10}
|
|
||||||
itemSize={grid?.itemSize || 200}
|
|
||||||
itemType={LibraryItem.ALBUM}
|
|
||||||
key={`album-list-${server?.id}-${display}`}
|
|
||||||
loading={itemCount === undefined || itemCount === null}
|
|
||||||
minimumBatchSize={40}
|
|
||||||
onScroll={handleGridScroll}
|
|
||||||
ref={gridRef}
|
|
||||||
route={{
|
|
||||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
|
||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
|
||||||
}}
|
|
||||||
width={width}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AutoSizer>
|
|
||||||
</VirtualGridAutoSizerContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,551 +1,37 @@
|
|||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
|
||||||
|
|
||||||
import { openModal } from '@mantine/modals';
|
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import { MouseEvent, MutableRefObject, useCallback, useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-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 { SubsonicAlbumFilters } from '/@/renderer/features/albums/components/subsonic-album-filters';
|
|
||||||
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
|
||||||
import { FilterButton } from '/@/renderer/features/shared/components/filter-button';
|
|
||||||
import { FolderButton } from '/@/renderer/features/shared/components/folder-button';
|
|
||||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||||
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
|
import { ListFilters } from '/@/renderer/features/shared/components/list-filters';
|
||||||
import { OrderToggleButton } from '/@/renderer/features/shared/components/order-toggle-button';
|
import { ListMusicFolderDropdown } from '/@/renderer/features/shared/components/list-music-folder-dropdown';
|
||||||
import { RefreshButton } from '/@/renderer/features/shared/components/refresh-button';
|
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||||
|
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||||
|
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh';
|
|
||||||
import {
|
|
||||||
AlbumListFilter,
|
|
||||||
useCurrentServer,
|
|
||||||
useListStoreActions,
|
|
||||||
useListStoreByKey,
|
|
||||||
} from '/@/renderer/store';
|
|
||||||
import { Button } from '/@/shared/components/button/button';
|
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { AlbumListSort, LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import {
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
AlbumListQuery,
|
|
||||||
AlbumListSort,
|
|
||||||
LibraryItem,
|
|
||||||
ServerType,
|
|
||||||
SortOrder,
|
|
||||||
} from '/@/shared/types/domain-types';
|
|
||||||
import { ListDisplayType, Play, TableColumn } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
const FILTERS = {
|
export const AlbumListHeaderFilters = () => {
|
||||||
jellyfin: [
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.ASC,
|
|
||||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.ALBUM_ARTIST,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.communityRating', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.COMMUNITY_RATING,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.criticRating', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.CRITIC_RATING,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.ASC,
|
|
||||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.NAME,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.playCount', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.PLAY_COUNT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.ASC,
|
|
||||||
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.RANDOM,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.RECENTLY_ADDED,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.releaseDate', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.RELEASE_DATE,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
navidrome: [
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.ASC,
|
|
||||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.ALBUM_ARTIST,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.ASC,
|
|
||||||
name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.ARTIST,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.DURATION,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.explicitStatus', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.EXPLICIT_STATUS,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.PLAY_COUNT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.ASC,
|
|
||||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.NAME,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.ASC,
|
|
||||||
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.RANDOM,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.RATING,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.RECENTLY_ADDED,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.RECENTLY_PLAYED,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.SONG_COUNT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.favorited', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.FAVORITED,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.YEAR,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
subsonic: [
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.ASC,
|
|
||||||
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.ALBUM_ARTIST,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.PLAY_COUNT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.ASC,
|
|
||||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.NAME,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.ASC,
|
|
||||||
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.RANDOM,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.RECENTLY_ADDED,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.RECENTLY_PLAYED,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.favorited', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.FAVORITED,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.DESC,
|
|
||||||
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
|
|
||||||
value: AlbumListSort.YEAR,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
interface AlbumListHeaderFiltersProps {
|
|
||||||
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
|
|
||||||
itemCount: number | undefined;
|
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AlbumListHeaderFilters = ({
|
|
||||||
gridRef,
|
|
||||||
itemCount,
|
|
||||||
tableRef,
|
|
||||||
}: AlbumListHeaderFiltersProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { customFilters, handlePlay, pageKey } = useListContext();
|
|
||||||
const server = useCurrentServer();
|
|
||||||
const { setDisplayType, setFilter, setGrid, setTable } = useListStoreActions();
|
|
||||||
const { display, filter, grid, table } = useListStoreByKey<AlbumListQuery>({
|
|
||||||
filter: customFilters,
|
|
||||||
key: pageKey,
|
|
||||||
});
|
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
|
|
||||||
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
|
|
||||||
itemCount,
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
server,
|
|
||||||
});
|
|
||||||
|
|
||||||
const musicFoldersQuery = useQuery(
|
|
||||||
sharedQueries.musicFolders({ query: null, serverId: server?.id }),
|
|
||||||
);
|
|
||||||
|
|
||||||
const sortByLabel =
|
|
||||||
(server?.type &&
|
|
||||||
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filter.sortBy)
|
|
||||||
?.name) ||
|
|
||||||
'Unknown';
|
|
||||||
|
|
||||||
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.GRID;
|
|
||||||
|
|
||||||
const onFilterChange = useCallback(
|
|
||||||
(filter: AlbumListFilter) => {
|
|
||||||
if (isGrid) {
|
|
||||||
handleRefreshGrid(gridRef, {
|
|
||||||
...filter,
|
|
||||||
...customFilters,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
handleRefreshTable(tableRef, {
|
|
||||||
...filter,
|
|
||||||
...customFilters,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[customFilters, gridRef, handleRefreshGrid, handleRefreshTable, isGrid, tableRef],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleOpenFiltersModal = () => {
|
|
||||||
let FilterComponent;
|
|
||||||
|
|
||||||
switch (server?.type) {
|
|
||||||
case ServerType.JELLYFIN:
|
|
||||||
FilterComponent = JellyfinAlbumFilters;
|
|
||||||
break;
|
|
||||||
case ServerType.NAVIDROME:
|
|
||||||
FilterComponent = NavidromeAlbumFilters;
|
|
||||||
break;
|
|
||||||
case ServerType.SUBSONIC:
|
|
||||||
FilterComponent = SubsonicAlbumFilters;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!FilterComponent) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
openModal({
|
|
||||||
children: (
|
|
||||||
<FilterComponent
|
|
||||||
customFilters={customFilters}
|
|
||||||
disableArtistFilter={!!customFilters}
|
|
||||||
onFilterChange={onFilterChange}
|
|
||||||
pageKey={pageKey}
|
|
||||||
serverId={server?.id}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
title: 'Album Filters',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.albums.list(server?.id || '') });
|
|
||||||
onFilterChange(filter);
|
|
||||||
}, [filter, onFilterChange, queryClient, server?.id]);
|
|
||||||
|
|
||||||
const handleSetSortBy = useCallback(
|
|
||||||
(e: MouseEvent<HTMLButtonElement>) => {
|
|
||||||
if (!e.currentTarget?.value || !server?.type) return;
|
|
||||||
|
|
||||||
const sortOrder = FILTERS[server.type as keyof typeof FILTERS].find(
|
|
||||||
(f) => f.value === e.currentTarget.value,
|
|
||||||
)?.defaultOrder;
|
|
||||||
|
|
||||||
const updatedFilters = setFilter({
|
|
||||||
customFilters,
|
|
||||||
data: {
|
|
||||||
sortBy: e.currentTarget.value as AlbumListSort,
|
|
||||||
sortOrder: sortOrder || SortOrder.ASC,
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
},
|
|
||||||
[customFilters, onFilterChange, pageKey, server?.type, setFilter],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSetMusicFolder = useCallback(
|
|
||||||
(e: MouseEvent<HTMLButtonElement>) => {
|
|
||||||
if (!e.currentTarget?.value) return;
|
|
||||||
|
|
||||||
let updatedFilters: AlbumListFilter | null = null;
|
|
||||||
if (e.currentTarget.value === String(filter.musicFolderId)) {
|
|
||||||
updatedFilters = setFilter({
|
|
||||||
customFilters,
|
|
||||||
data: { musicFolderId: undefined },
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
} else {
|
|
||||||
updatedFilters = setFilter({
|
|
||||||
customFilters,
|
|
||||||
data: { musicFolderId: e.currentTarget.value },
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
},
|
|
||||||
[filter.musicFolderId, onFilterChange, setFilter, customFilters, pageKey],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleToggleSortOrder = useCallback(() => {
|
|
||||||
const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
|
||||||
const updatedFilters = setFilter({
|
|
||||||
customFilters,
|
|
||||||
data: { sortOrder: newSortOrder },
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
}, [customFilters, filter.sortOrder, onFilterChange, pageKey, setFilter]);
|
|
||||||
|
|
||||||
const handleItemSize = (e: number) => {
|
|
||||||
if (isGrid) {
|
|
||||||
setGrid({ data: { itemSize: e }, key: pageKey });
|
|
||||||
} else {
|
|
||||||
setTable({ data: { rowHeight: e }, key: pageKey });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedHandleItemSize = debounce(handleItemSize, 20);
|
|
||||||
|
|
||||||
const handleItemGap = (e: number) => {
|
|
||||||
setGrid({ data: { itemGap: e }, key: pageKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSetViewType = useCallback(
|
|
||||||
(displayType: ListDisplayType) => {
|
|
||||||
setDisplayType({ data: displayType, key: pageKey });
|
|
||||||
},
|
|
||||||
[pageKey, setDisplayType],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTableColumns = (values: string[]) => {
|
|
||||||
const existingColumns = table.columns;
|
|
||||||
|
|
||||||
if (values.length === 0) {
|
|
||||||
return setTable({
|
|
||||||
data: { columns: [] },
|
|
||||||
key: pageKey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If adding a column
|
|
||||||
if (values.length > existingColumns.length) {
|
|
||||||
const newColumn = { column: values[values.length - 1] as TableColumn, width: 100 };
|
|
||||||
|
|
||||||
setTable({ data: { columns: [...existingColumns, newColumn] }, key: pageKey });
|
|
||||||
} else {
|
|
||||||
// If removing a column
|
|
||||||
const removed = existingColumns.filter((column) => !values.includes(column.column));
|
|
||||||
const newColumns = existingColumns.filter((column) => !removed.includes(column));
|
|
||||||
|
|
||||||
setTable({ data: { columns: newColumns }, key: pageKey });
|
|
||||||
}
|
|
||||||
|
|
||||||
return tableRef.current?.api.sizeColumnsToFit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAutoFitColumns = (autoFitColumns: boolean) => {
|
|
||||||
setTable({ data: { autoFit: autoFitColumns }, key: pageKey });
|
|
||||||
|
|
||||||
if (autoFitColumns) {
|
|
||||||
tableRef.current?.api.sizeColumnsToFit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFilterApplied = useMemo(() => {
|
|
||||||
const isNavidromeFilterApplied =
|
|
||||||
server?.type === ServerType.NAVIDROME &&
|
|
||||||
((filter?._custom?.navidrome &&
|
|
||||||
Object.values(filter?._custom?.navidrome).some((value) => value !== undefined)) ||
|
|
||||||
// Compilation is always valid
|
|
||||||
filter.compilation !== undefined);
|
|
||||||
|
|
||||||
const isJellyfinFilterApplied =
|
|
||||||
server?.type === ServerType.JELLYFIN &&
|
|
||||||
((filter?._custom?.jellyfin &&
|
|
||||||
Object.values(filter?._custom?.jellyfin).some((value) => value !== undefined)) ||
|
|
||||||
// Compilation filter is only valid when on the artist page
|
|
||||||
(filter.compilation !== undefined && customFilters?.artistIds));
|
|
||||||
|
|
||||||
const isSubsonicFilterApplied =
|
|
||||||
server?.type === ServerType.SUBSONIC && (filter.maxYear || filter.minYear);
|
|
||||||
|
|
||||||
return (
|
|
||||||
isNavidromeFilterApplied ||
|
|
||||||
isJellyfinFilterApplied ||
|
|
||||||
isSubsonicFilterApplied ||
|
|
||||||
filter.genres?.length ||
|
|
||||||
filter.favorite !== undefined ||
|
|
||||||
// If we are on the artist page, the artist id filter should not be active
|
|
||||||
(filter.artistIds?.length && !(customFilters?.artistIds as any | undefined)?.length)
|
|
||||||
);
|
|
||||||
}, [
|
|
||||||
customFilters?.artistIds,
|
|
||||||
filter?._custom?.jellyfin,
|
|
||||||
filter?._custom?.navidrome,
|
|
||||||
filter.artistIds?.length,
|
|
||||||
filter.compilation,
|
|
||||||
filter.favorite,
|
|
||||||
filter.genres?.length,
|
|
||||||
filter.maxYear,
|
|
||||||
filter.minYear,
|
|
||||||
server?.type,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const isFolderFilterApplied = useMemo(() => {
|
|
||||||
return filter.musicFolderId !== undefined;
|
|
||||||
}, [filter.musicFolderId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justify="space-between">
|
<Flex justify="space-between">
|
||||||
<Group gap="sm" ref={cq.ref} w="100%">
|
<Group gap="sm" ref={cq.ref} w="100%">
|
||||||
<DropdownMenu position="bottom-start">
|
<ListSortByDropdown
|
||||||
<DropdownMenu.Target>
|
defaultSortByValue={AlbumListSort.NAME}
|
||||||
<Button variant="subtle">{sortByLabel}</Button>
|
itemType={LibraryItem.ALBUM}
|
||||||
</DropdownMenu.Target>
|
listKey={ItemListKey.ALBUM}
|
||||||
<DropdownMenu.Dropdown>
|
/>
|
||||||
{FILTERS[server?.type as keyof typeof FILTERS].map((f) => (
|
|
||||||
<DropdownMenu.Item
|
|
||||||
isSelected={f.value === filter.sortBy}
|
|
||||||
key={`filter-${f.name}`}
|
|
||||||
onClick={handleSetSortBy}
|
|
||||||
value={f.value}
|
|
||||||
>
|
|
||||||
{f.name}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
))}
|
|
||||||
</DropdownMenu.Dropdown>
|
|
||||||
</DropdownMenu>
|
|
||||||
<Divider orientation="vertical" />
|
<Divider orientation="vertical" />
|
||||||
<OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} />
|
<ListSortOrderToggleButton listKey={ItemListKey.ALBUM} />
|
||||||
{server?.type === ServerType.JELLYFIN && (
|
<ListMusicFolderDropdown listKey={ItemListKey.ALBUM} />
|
||||||
<>
|
<ListFilters itemType={LibraryItem.ALBUM} />
|
||||||
<Divider orientation="vertical" />
|
<ListRefreshButton listKey={ItemListKey.ALBUM} />
|
||||||
<DropdownMenu position="bottom-start">
|
|
||||||
<DropdownMenu.Target>
|
|
||||||
<FolderButton isActive={!!isFolderFilterApplied} />
|
|
||||||
</DropdownMenu.Target>
|
|
||||||
<DropdownMenu.Dropdown>
|
|
||||||
{musicFoldersQuery.data?.items.map((folder) => (
|
|
||||||
<DropdownMenu.Item
|
|
||||||
isSelected={filter.musicFolderId === folder.id}
|
|
||||||
key={`musicFolder-${folder.id}`}
|
|
||||||
onClick={handleSetMusicFolder}
|
|
||||||
value={folder.id}
|
|
||||||
>
|
|
||||||
{folder.name}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
))}
|
|
||||||
</DropdownMenu.Dropdown>
|
|
||||||
</DropdownMenu>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<FilterButton isActive={!!isFilterApplied} onClick={handleOpenFiltersModal} />
|
|
||||||
<RefreshButton onClick={handleRefresh} />
|
|
||||||
<DropdownMenu position="bottom-start">
|
|
||||||
<DropdownMenu.Target>
|
|
||||||
<MoreButton />
|
|
||||||
</DropdownMenu.Target>
|
|
||||||
<DropdownMenu.Dropdown>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
leftSection={<Icon icon="mediaPlay" />}
|
|
||||||
onClick={() => handlePlay?.({ playType: Play.NOW })}
|
|
||||||
>
|
|
||||||
{t('player.play', { postProcess: 'sentenceCase' })}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
leftSection={<Icon icon="mediaPlayLast" />}
|
|
||||||
onClick={() => handlePlay?.({ playType: Play.LAST })}
|
|
||||||
>
|
|
||||||
{t('player.addLast', { postProcess: 'sentenceCase' })}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
leftSection={<Icon icon="mediaPlayNext" />}
|
|
||||||
onClick={() => handlePlay?.({ playType: Play.NEXT })}
|
|
||||||
>
|
|
||||||
{t('player.addNext', { postProcess: 'sentenceCase' })}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Divider />
|
|
||||||
<DropdownMenu.Item
|
|
||||||
leftSection={<Icon icon="refresh" />}
|
|
||||||
onClick={handleRefresh}
|
|
||||||
>
|
|
||||||
{t('common.refresh', { postProcess: 'sentenceCase' })}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Dropdown>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<ListConfigMenu
|
<ListConfigMenu
|
||||||
autoFitColumns={table.autoFit}
|
listKey={ItemListKey.ALBUM}
|
||||||
disabledViewTypes={[ListDisplayType.LIST]}
|
|
||||||
displayType={display}
|
|
||||||
itemGap={grid?.itemGap || 0}
|
|
||||||
itemSize={isGrid ? grid?.itemSize || 0 : table.rowHeight}
|
|
||||||
onChangeAutoFitColumns={handleAutoFitColumns}
|
|
||||||
onChangeDisplayType={handleSetViewType}
|
|
||||||
onChangeItemGap={handleItemGap}
|
|
||||||
onChangeItemSize={debouncedHandleItemSize}
|
|
||||||
onChangeTableColumns={handleTableColumns}
|
|
||||||
tableColumns={table?.columns.map((column) => column.column)}
|
|
||||||
tableColumnsData={ALBUM_TABLE_COLUMNS}
|
tableColumnsData={ALBUM_TABLE_COLUMNS}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -1,95 +1,40 @@
|
|||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
|
||||||
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import { type ChangeEvent, type MutableRefObject, useEffect, useRef } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
|
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
|
||||||
import { FilterBar } from '/@/renderer/features/shared/components/filter-bar';
|
import { FilterBar } from '/@/renderer/features/shared/components/filter-bar';
|
||||||
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
||||||
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
|
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
|
||||||
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
|
|
||||||
import { AlbumListFilter, useCurrentServer, usePlayButtonBehavior } from '/@/renderer/store';
|
|
||||||
import { titleCase } from '/@/renderer/utils';
|
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { AlbumListQuery, LibraryItem } from '/@/shared/types/domain-types';
|
|
||||||
|
|
||||||
interface AlbumListHeaderProps {
|
interface AlbumListHeaderProps {
|
||||||
genreId?: string;
|
|
||||||
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
|
|
||||||
itemCount?: number;
|
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlbumListHeader = ({
|
export const AlbumListHeader = ({ title }: AlbumListHeaderProps) => {
|
||||||
genreId,
|
|
||||||
gridRef,
|
|
||||||
itemCount,
|
|
||||||
tableRef,
|
|
||||||
title,
|
|
||||||
}: AlbumListHeaderProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const server = useCurrentServer();
|
|
||||||
const cq = useContainerQuery();
|
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
|
||||||
const genreRef = useRef<string | undefined>(undefined);
|
|
||||||
const { filter, handlePlay, refresh, search } = useDisplayRefresh<AlbumListQuery>({
|
|
||||||
gridRef,
|
|
||||||
itemCount,
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
server,
|
|
||||||
tableRef,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
|
const { itemCount } = useListContext();
|
||||||
const updatedFilters = search(e) as AlbumListFilter;
|
const pageTitle = title || t('page.albumList.title', { postProcess: 'titleCase' });
|
||||||
|
|
||||||
refresh(updatedFilters);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (genreRef.current && genreRef.current !== genreId) {
|
|
||||||
refresh(filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
genreRef.current = genreId;
|
|
||||||
}, [filter, genreId, refresh, tableRef]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={0} ref={cq.ref}>
|
<Stack gap={0}>
|
||||||
<PageHeader backgroundColor="var(--theme-colors-background)">
|
<PageHeader backgroundColor="var(--theme-colors-background)">
|
||||||
<Flex justify="space-between" w="100%">
|
<LibraryHeaderBar>
|
||||||
<LibraryHeaderBar>
|
<LibraryHeaderBar.PlayButton />
|
||||||
<LibraryHeaderBar.PlayButton
|
<LibraryHeaderBar.Title>{pageTitle}</LibraryHeaderBar.Title>
|
||||||
onClick={() => handlePlay?.({ playType: playButtonBehavior })}
|
<LibraryHeaderBar.Badge isLoading={!itemCount}>
|
||||||
/>
|
{itemCount}
|
||||||
<LibraryHeaderBar.Title>
|
</LibraryHeaderBar.Badge>
|
||||||
{title ||
|
</LibraryHeaderBar>
|
||||||
titleCase(t('page.albumList.title', { postProcess: 'titleCase' }))}
|
<Group>
|
||||||
</LibraryHeaderBar.Title>
|
<SearchInput />
|
||||||
<LibraryHeaderBar.Badge
|
</Group>
|
||||||
isLoading={itemCount === null || itemCount === undefined}
|
|
||||||
>
|
|
||||||
{itemCount}
|
|
||||||
</LibraryHeaderBar.Badge>
|
|
||||||
</LibraryHeaderBar>
|
|
||||||
<Group>
|
|
||||||
<SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} />
|
|
||||||
</Group>
|
|
||||||
</Flex>
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<AlbumListHeaderFilters
|
<AlbumListHeaderFilters />
|
||||||
gridRef={gridRef}
|
|
||||||
itemCount={itemCount}
|
|
||||||
tableRef={tableRef}
|
|
||||||
/>
|
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { UseSuspenseQueryOptions } from '@tanstack/react-query';
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import {
|
import { useItemListInfiniteLoader } from '/@/renderer/components/item-list/helpers/item-list-infinite-loader';
|
||||||
InfiniteListProps,
|
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||||
useItemListInfiniteLoader,
|
|
||||||
} from '/@/renderer/components/item-list/helpers/item-list-infinite-loader';
|
|
||||||
import { ItemGridList } from '/@/renderer/components/item-list/item-grid-list/item-grid-list';
|
import { ItemGridList } from '/@/renderer/components/item-list/item-grid-list/item-grid-list';
|
||||||
|
import { ItemListGridComponentProps } from '/@/renderer/components/item-list/types';
|
||||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
import {
|
import {
|
||||||
AlbumListQuery,
|
AlbumListQuery,
|
||||||
@@ -14,16 +13,20 @@ import {
|
|||||||
LibraryItem,
|
LibraryItem,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
interface AlbumListInfiniteGridProps extends InfiniteListProps<AlbumListQuery> {}
|
interface AlbumListInfiniteGridProps extends ItemListGridComponentProps<AlbumListQuery> {}
|
||||||
|
|
||||||
export const AlbumListInfiniteGrid = forwardRef<any, AlbumListInfiniteGridProps>(
|
export const AlbumListInfiniteGrid = forwardRef<any, AlbumListInfiniteGridProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
gap = 'md',
|
||||||
|
itemsPerPage = 100,
|
||||||
|
itemsPerRow,
|
||||||
query = {
|
query = {
|
||||||
sortBy: AlbumListSort.NAME,
|
sortBy: AlbumListSort.NAME,
|
||||||
sortOrder: SortOrder.ASC,
|
sortOrder: SortOrder.ASC,
|
||||||
},
|
},
|
||||||
|
saveScrollOffset = true,
|
||||||
serverId,
|
serverId,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -36,18 +39,31 @@ export const AlbumListInfiniteGrid = forwardRef<any, AlbumListInfiniteGridProps>
|
|||||||
const listQueryFn = api.controller.getAlbumList;
|
const listQueryFn = api.controller.getAlbumList;
|
||||||
|
|
||||||
const { data, onRangeChanged } = useItemListInfiniteLoader({
|
const { data, onRangeChanged } = useItemListInfiniteLoader({
|
||||||
itemsPerPage: 100,
|
eventKey: ItemListKey.ALBUM,
|
||||||
|
itemsPerPage,
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
listCountQuery,
|
listCountQuery,
|
||||||
listQueryFn,
|
listQueryFn,
|
||||||
query,
|
query,
|
||||||
serverId,
|
serverId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
|
||||||
|
enabled: saveScrollOffset,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ItemGridList
|
<ItemGridList
|
||||||
data={data || []}
|
data={data}
|
||||||
|
gap={gap}
|
||||||
|
initialTop={{
|
||||||
|
to: scrollOffset ?? 0,
|
||||||
|
type: 'offset',
|
||||||
|
}}
|
||||||
|
itemsPerRow={itemsPerRow}
|
||||||
itemType={LibraryItem.ALBUM}
|
itemType={LibraryItem.ALBUM}
|
||||||
onRangeChanged={onRangeChanged}
|
onRangeChanged={onRangeChanged}
|
||||||
|
onScrollEnd={handleOnScrollEnd}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { UseSuspenseQueryOptions } from '@tanstack/react-query';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { useItemListInfiniteLoader } from '/@/renderer/components/item-list/helpers/item-list-infinite-loader';
|
||||||
|
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||||
|
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||||
|
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||||
|
import { ItemListTableComponentProps } from '/@/renderer/components/item-list/types';
|
||||||
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
|
import {
|
||||||
|
AlbumListQuery,
|
||||||
|
AlbumListSort,
|
||||||
|
LibraryItem,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
interface AlbumListInfiniteTableProps extends ItemListTableComponentProps<AlbumListQuery> {}
|
||||||
|
|
||||||
|
export const AlbumListInfiniteTable = forwardRef<any, AlbumListInfiniteTableProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
columns,
|
||||||
|
enableAlternateRowColors = false,
|
||||||
|
enableHorizontalBorders = false,
|
||||||
|
enableRowHoverHighlight = true,
|
||||||
|
enableVerticalBorders = false,
|
||||||
|
itemsPerPage = 100,
|
||||||
|
query = {
|
||||||
|
sortBy: AlbumListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
},
|
||||||
|
saveScrollOffset = true,
|
||||||
|
serverId,
|
||||||
|
size = 'default',
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const listCountQuery = albumQueries.listCount({
|
||||||
|
query: { ...query },
|
||||||
|
serverId: serverId,
|
||||||
|
}) as UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
|
||||||
|
|
||||||
|
const listQueryFn = api.controller.getAlbumList;
|
||||||
|
|
||||||
|
const { data, onRangeChanged } = useItemListInfiniteLoader({
|
||||||
|
eventKey: ItemListKey.ALBUM,
|
||||||
|
itemsPerPage,
|
||||||
|
itemType: LibraryItem.ALBUM,
|
||||||
|
listCountQuery,
|
||||||
|
listQueryFn,
|
||||||
|
query,
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
|
||||||
|
enabled: saveScrollOffset,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemTableList
|
||||||
|
CellComponent={ItemTableListColumn}
|
||||||
|
columns={columns}
|
||||||
|
data={data}
|
||||||
|
enableAlternateRowColors={enableAlternateRowColors}
|
||||||
|
enableHorizontalBorders={enableHorizontalBorders}
|
||||||
|
enableRowHoverHighlight={enableRowHoverHighlight}
|
||||||
|
enableVerticalBorders={enableVerticalBorders}
|
||||||
|
initialTop={{
|
||||||
|
to: scrollOffset ?? 0,
|
||||||
|
type: 'offset',
|
||||||
|
}}
|
||||||
|
itemType={LibraryItem.ALBUM}
|
||||||
|
onRangeChanged={onRangeChanged}
|
||||||
|
onScrollEnd={handleOnScrollEnd}
|
||||||
|
ref={ref}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -2,13 +2,12 @@ import { UseSuspenseQueryOptions } from '@tanstack/react-query';
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import {
|
import { useItemListPaginatedLoader } from '/@/renderer/components/item-list/helpers/item-list-paginated-loader';
|
||||||
PaginatedListProps,
|
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||||
useItemListPaginatedLoader,
|
|
||||||
} from '/@/renderer/components/item-list/helpers/item-list-paginated-loader';
|
|
||||||
import { ItemGridList } from '/@/renderer/components/item-list/item-grid-list/item-grid-list';
|
import { ItemGridList } from '/@/renderer/components/item-list/item-grid-list/item-grid-list';
|
||||||
import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination';
|
import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination';
|
||||||
import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination';
|
import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination';
|
||||||
|
import { ItemListGridComponentProps } from '/@/renderer/components/item-list/types';
|
||||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
import {
|
import {
|
||||||
AlbumListQuery,
|
AlbumListQuery,
|
||||||
@@ -17,17 +16,18 @@ import {
|
|||||||
SortOrder,
|
SortOrder,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
interface AlbumListPaginatedGridProps extends PaginatedListProps<AlbumListQuery> {}
|
interface AlbumListPaginatedGridProps extends ItemListGridComponentProps<AlbumListQuery> {}
|
||||||
|
|
||||||
export const AlbumListPaginatedGrid = forwardRef<any, AlbumListPaginatedGridProps>(
|
export const AlbumListPaginatedGrid = forwardRef<any, AlbumListPaginatedGridProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
initialPage,
|
gap = 'md',
|
||||||
itemsPerPage = 100,
|
itemsPerPage = 100,
|
||||||
query = {
|
query = {
|
||||||
sortBy: AlbumListSort.NAME,
|
sortBy: AlbumListSort.NAME,
|
||||||
sortOrder: SortOrder.ASC,
|
sortOrder: SortOrder.ASC,
|
||||||
},
|
},
|
||||||
|
saveScrollOffset = true,
|
||||||
serverId,
|
serverId,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@@ -39,7 +39,7 @@ export const AlbumListPaginatedGrid = forwardRef<any, AlbumListPaginatedGridProp
|
|||||||
|
|
||||||
const listQueryFn = api.controller.getAlbumList;
|
const listQueryFn = api.controller.getAlbumList;
|
||||||
|
|
||||||
const { currentPage, onChange } = useItemListPagination({ initialPage });
|
const { currentPage, onChange } = useItemListPagination();
|
||||||
|
|
||||||
const { data, pageCount, totalItemCount } = useItemListPaginatedLoader({
|
const { data, pageCount, totalItemCount } = useItemListPaginatedLoader({
|
||||||
currentPage,
|
currentPage,
|
||||||
@@ -50,6 +50,10 @@ export const AlbumListPaginatedGrid = forwardRef<any, AlbumListPaginatedGridProp
|
|||||||
serverId,
|
serverId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
|
||||||
|
enabled: saveScrollOffset,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ItemListWithPagination
|
<ItemListWithPagination
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
@@ -58,7 +62,17 @@ export const AlbumListPaginatedGrid = forwardRef<any, AlbumListPaginatedGridProp
|
|||||||
pageCount={pageCount}
|
pageCount={pageCount}
|
||||||
totalItemCount={totalItemCount}
|
totalItemCount={totalItemCount}
|
||||||
>
|
>
|
||||||
<ItemGridList data={data || []} itemType={LibraryItem.ALBUM} ref={ref} />
|
<ItemGridList
|
||||||
|
data={data || []}
|
||||||
|
gap={gap}
|
||||||
|
initialTop={{
|
||||||
|
to: scrollOffset ?? 0,
|
||||||
|
type: 'offset',
|
||||||
|
}}
|
||||||
|
itemType={LibraryItem.ALBUM}
|
||||||
|
onScrollEnd={handleOnScrollEnd}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
</ItemListWithPagination>
|
</ItemListWithPagination>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { UseSuspenseQueryOptions } from '@tanstack/react-query';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { useItemListPaginatedLoader } from '/@/renderer/components/item-list/helpers/item-list-paginated-loader';
|
||||||
|
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||||
|
import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination';
|
||||||
|
import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination';
|
||||||
|
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||||
|
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||||
|
import { ItemListTableComponentProps } from '/@/renderer/components/item-list/types';
|
||||||
|
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
||||||
|
import {
|
||||||
|
AlbumListQuery,
|
||||||
|
AlbumListSort,
|
||||||
|
LibraryItem,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
interface AlbumListPaginatedTableProps extends ItemListTableComponentProps<AlbumListQuery> {}
|
||||||
|
|
||||||
|
export const AlbumListPaginatedTable = forwardRef<any, AlbumListPaginatedTableProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
columns,
|
||||||
|
enableAlternateRowColors = false,
|
||||||
|
enableHorizontalBorders = false,
|
||||||
|
enableRowHoverHighlight = true,
|
||||||
|
enableVerticalBorders = false,
|
||||||
|
itemsPerPage = 100,
|
||||||
|
query = {
|
||||||
|
sortBy: AlbumListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
},
|
||||||
|
saveScrollOffset = true,
|
||||||
|
serverId,
|
||||||
|
size = 'default',
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const listCountQuery = albumQueries.listCount({
|
||||||
|
query: { ...query },
|
||||||
|
serverId: serverId,
|
||||||
|
}) as UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
|
||||||
|
|
||||||
|
const listQueryFn = api.controller.getAlbumList;
|
||||||
|
|
||||||
|
const { currentPage, onChange } = useItemListPagination();
|
||||||
|
|
||||||
|
const { data, pageCount, totalItemCount } = useItemListPaginatedLoader({
|
||||||
|
currentPage,
|
||||||
|
itemsPerPage,
|
||||||
|
listCountQuery,
|
||||||
|
listQueryFn,
|
||||||
|
query,
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
|
||||||
|
enabled: saveScrollOffset,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemListWithPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
onChange={onChange}
|
||||||
|
pageCount={pageCount}
|
||||||
|
totalItemCount={totalItemCount}
|
||||||
|
>
|
||||||
|
<ItemTableList
|
||||||
|
CellComponent={ItemTableListColumn}
|
||||||
|
columns={columns}
|
||||||
|
data={data || []}
|
||||||
|
enableAlternateRowColors={enableAlternateRowColors}
|
||||||
|
enableHorizontalBorders={enableHorizontalBorders}
|
||||||
|
enableRowHoverHighlight={enableRowHoverHighlight}
|
||||||
|
enableVerticalBorders={enableVerticalBorders}
|
||||||
|
initialTop={{
|
||||||
|
to: scrollOffset ?? 0,
|
||||||
|
type: 'offset',
|
||||||
|
}}
|
||||||
|
itemType={LibraryItem.ALBUM}
|
||||||
|
onScrollEnd={handleOnScrollEnd}
|
||||||
|
ref={ref}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
</ItemListWithPagination>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
|
|
||||||
import { VirtualTable } from '/@/renderer/components/virtual-table';
|
|
||||||
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
|
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
|
||||||
import { ALBUM_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
|
||||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
|
||||||
|
|
||||||
export const AlbumListTableView = ({ itemCount, tableRef }: any) => {
|
|
||||||
const server = useCurrentServer();
|
|
||||||
const { customFilters, id, pageKey } = useListContext();
|
|
||||||
|
|
||||||
const tableProps = useVirtualTable({
|
|
||||||
contextMenu: ALBUM_CONTEXT_MENU_ITEMS,
|
|
||||||
customFilters,
|
|
||||||
isSearchParams: Boolean(id),
|
|
||||||
itemCount,
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
pageKey,
|
|
||||||
server,
|
|
||||||
tableRef,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualGridAutoSizerContainer>
|
|
||||||
<VirtualTable
|
|
||||||
// https://github.com/ag-grid/ag-grid/issues/5284
|
|
||||||
// Key is used to force remount of table when display, rowHeight, or server changes
|
|
||||||
key={`table-${tableProps.rowHeight}-${server?.id}`}
|
|
||||||
ref={tableRef}
|
|
||||||
{...tableProps}
|
|
||||||
/>
|
|
||||||
</VirtualGridAutoSizerContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -4,10 +4,11 @@ import { useMemo } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||||
|
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
||||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||||
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
||||||
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
||||||
import { AlbumListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store';
|
import { AlbumListFilter } from '/@/renderer/store';
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
@@ -17,7 +18,6 @@ import { Text } from '/@/shared/components/text/text';
|
|||||||
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
|
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
|
||||||
import {
|
import {
|
||||||
AlbumArtistListSort,
|
AlbumArtistListSort,
|
||||||
AlbumListQuery,
|
|
||||||
GenreListSort,
|
GenreListSort,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
@@ -27,30 +27,31 @@ interface JellyfinAlbumFiltersProps {
|
|||||||
customFilters?: Partial<AlbumListFilter>;
|
customFilters?: Partial<AlbumListFilter>;
|
||||||
disableArtistFilter?: boolean;
|
disableArtistFilter?: boolean;
|
||||||
onFilterChange: (filters: AlbumListFilter) => void;
|
onFilterChange: (filters: AlbumListFilter) => void;
|
||||||
pageKey: string;
|
|
||||||
serverId: string;
|
serverId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const JellyfinAlbumFilters = ({
|
export const JellyfinAlbumFilters = ({
|
||||||
customFilters,
|
|
||||||
disableArtistFilter,
|
disableArtistFilter,
|
||||||
onFilterChange,
|
|
||||||
pageKey,
|
|
||||||
serverId,
|
serverId,
|
||||||
}: JellyfinAlbumFiltersProps) => {
|
}: JellyfinAlbumFiltersProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const filter = useListFilterByKey<AlbumListQuery>({ key: pageKey });
|
|
||||||
const { setFilter } = useListStoreActions();
|
const {
|
||||||
|
query,
|
||||||
|
setAlbumArtist,
|
||||||
|
setAlbumCompilation,
|
||||||
|
setAlbumFavorite,
|
||||||
|
setAlbumGenre,
|
||||||
|
setCustom,
|
||||||
|
setMaxAlbumYear,
|
||||||
|
setMinAlbumYear,
|
||||||
|
} = useAlbumListFilters();
|
||||||
|
|
||||||
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
|
// TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library
|
||||||
const genreListQuery = useQuery(
|
const genreListQuery = useQuery(
|
||||||
genresQueries.list({
|
genresQueries.list({
|
||||||
options: {
|
|
||||||
gcTime: 1000 * 60 * 2,
|
|
||||||
staleTime: 1000 * 60 * 1,
|
|
||||||
},
|
|
||||||
query: {
|
query: {
|
||||||
musicFolderId: filter?.musicFolderId,
|
musicFolderId: query.musicFolderId,
|
||||||
sortBy: GenreListSort.NAME,
|
sortBy: GenreListSort.NAME,
|
||||||
sortOrder: SortOrder.ASC,
|
sortOrder: SortOrder.ASC,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
@@ -74,106 +75,57 @@ export const JellyfinAlbumFilters = ({
|
|||||||
staleTime: 1000 * 60 * 1,
|
staleTime: 1000 * 60 * 1,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
folder: filter?.musicFolderId,
|
folder: query.musicFolderId,
|
||||||
type: LibraryItem.ALBUM,
|
type: LibraryItem.ALBUM,
|
||||||
},
|
},
|
||||||
serverId,
|
serverId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedTags = useMemo(() => {
|
|
||||||
return filter?._custom?.jellyfin?.Tags?.split('|');
|
|
||||||
}, [filter?._custom?.jellyfin?.Tags]);
|
|
||||||
|
|
||||||
const yesNoFilter = useMemo(() => {
|
const yesNoFilter = useMemo(() => {
|
||||||
const filters = [
|
const filters = [
|
||||||
{
|
{
|
||||||
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||||
onChange: (favorite?: boolean) => {
|
onChange: (favoriteValue?: boolean) => {
|
||||||
const updatedFilters = setFilter({
|
setAlbumFavorite(favoriteValue ?? null);
|
||||||
customFilters,
|
|
||||||
data: {
|
|
||||||
_custom: filter?._custom,
|
|
||||||
favorite,
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
},
|
},
|
||||||
value: filter?.favorite,
|
value: query.favorite,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
if (customFilters?.artistIds) {
|
if (query.artistIds?.length) {
|
||||||
filters.push({
|
filters.push({
|
||||||
label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
|
label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
|
||||||
onChange: (compilation?: boolean) => {
|
onChange: (compilationValue?: boolean) => {
|
||||||
const updatedFilters = setFilter({
|
setAlbumCompilation(compilationValue ?? null);
|
||||||
customFilters,
|
|
||||||
data: {
|
|
||||||
_custom: filter._custom,
|
|
||||||
compilation,
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
},
|
},
|
||||||
value: filter.compilation,
|
value: query.compilation,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return filters;
|
return filters;
|
||||||
}, [
|
}, [
|
||||||
customFilters,
|
|
||||||
filter._custom,
|
|
||||||
filter.compilation,
|
|
||||||
filter?.favorite,
|
|
||||||
onFilterChange,
|
|
||||||
pageKey,
|
|
||||||
setFilter,
|
|
||||||
t,
|
t,
|
||||||
|
query.favorite,
|
||||||
|
query.artistIds?.length,
|
||||||
|
query.compilation,
|
||||||
|
setAlbumFavorite,
|
||||||
|
setAlbumCompilation,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
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 year = e === '' ? undefined : (e as number);
|
||||||
customFilters,
|
setMinAlbumYear(year ?? null);
|
||||||
data: {
|
|
||||||
_custom: filter?._custom,
|
|
||||||
minYear: e === '' ? undefined : (e as number),
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
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 year = e === '' ? undefined : (e as number);
|
||||||
customFilters,
|
setMaxAlbumYear(year ?? null);
|
||||||
data: {
|
|
||||||
_custom: filter?._custom,
|
|
||||||
maxYear: e === '' ? undefined : (e as number),
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
const handleGenresFilter = debounce((e: string[] | undefined) => {
|
const handleGenresFilter = debounce((e: string[] | undefined) => {
|
||||||
const updatedFilters = setFilter({
|
setAlbumGenre(e ?? null);
|
||||||
customFilters,
|
|
||||||
data: {
|
|
||||||
_custom: filter?._custom,
|
|
||||||
genres: e,
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
const albumArtistListQuery = useQuery(
|
const albumArtistListQuery = useQuery(
|
||||||
@@ -201,70 +153,54 @@ export const JellyfinAlbumFilters = ({
|
|||||||
}, [albumArtistListQuery?.data?.items]);
|
}, [albumArtistListQuery?.data?.items]);
|
||||||
|
|
||||||
const handleAlbumArtistFilter = (e: null | string[]) => {
|
const handleAlbumArtistFilter = (e: null | string[]) => {
|
||||||
const updatedFilters = setFilter({
|
setAlbumArtist(e ?? null);
|
||||||
customFilters,
|
|
||||||
data: {
|
|
||||||
_custom: filter?._custom,
|
|
||||||
artistIds: e?.length ? e : undefined,
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTagFilter = debounce((e: string[] | undefined) => {
|
const handleTagFilter = debounce((e: string[] | undefined) => {
|
||||||
const updatedFilters = setFilter({
|
setCustom((prev) => ({
|
||||||
customFilters,
|
...prev,
|
||||||
data: {
|
[e?.join('|') || '']: e?.join('|') || undefined,
|
||||||
_custom: {
|
}));
|
||||||
...filter?._custom,
|
|
||||||
jellyfin: {
|
|
||||||
...filter?._custom?.jellyfin,
|
|
||||||
Tags: e?.join('|') || undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.SONG,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p="0.8rem">
|
<Stack p="0.8rem">
|
||||||
{yesNoFilter.map((filter) => (
|
{yesNoFilter.map((filter) => (
|
||||||
<Group justify="space-between" key={`nd-filter-${filter.label}`}>
|
<Group justify="space-between" key={`jf-filter-${filter.label}`}>
|
||||||
<Text>{filter.label}</Text>
|
<Text>{filter.label}</Text>
|
||||||
<YesNoSelect onChange={filter.onChange} size="xs" value={filter.value} />
|
<YesNoSelect
|
||||||
|
onChange={filter.onChange}
|
||||||
|
size="xs"
|
||||||
|
value={filter.value ?? undefined}
|
||||||
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
<Divider my="0.5rem" />
|
<Divider my="0.5rem" />
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
defaultValue={filter?.minYear}
|
defaultValue={query.minYear ?? undefined}
|
||||||
hideControls={false}
|
hideControls={false}
|
||||||
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
||||||
max={2300}
|
max={2300}
|
||||||
min={1700}
|
min={1700}
|
||||||
onChange={(e) => handleMinYearFilter(e)}
|
onChange={(e) => handleMinYearFilter(e)}
|
||||||
required={!!filter?.maxYear}
|
required={!!query.minYear}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
defaultValue={filter?.maxYear}
|
defaultValue={query.maxYear ?? undefined}
|
||||||
hideControls={false}
|
hideControls={false}
|
||||||
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
||||||
max={2300}
|
max={2300}
|
||||||
min={1700}
|
min={1700}
|
||||||
onChange={(e) => handleMaxYearFilter(e)}
|
onChange={(e) => handleMaxYearFilter(e)}
|
||||||
required={!!filter?.minYear}
|
required={!!query.minYear}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<MultiSelectWithInvalidData
|
<MultiSelectWithInvalidData
|
||||||
clearable
|
clearable
|
||||||
data={genreList}
|
data={genreList}
|
||||||
defaultValue={filter.genres}
|
defaultValue={query.genres ?? undefined}
|
||||||
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
|
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
|
||||||
onChange={handleGenresFilter}
|
onChange={handleGenresFilter}
|
||||||
searchable
|
searchable
|
||||||
@@ -275,7 +211,7 @@ export const JellyfinAlbumFilters = ({
|
|||||||
<MultiSelectWithInvalidData
|
<MultiSelectWithInvalidData
|
||||||
clearable
|
clearable
|
||||||
data={selectableAlbumArtists}
|
data={selectableAlbumArtists}
|
||||||
defaultValue={filter?.artistIds}
|
defaultValue={query.artistIds ?? undefined}
|
||||||
disabled={disableArtistFilter}
|
disabled={disableArtistFilter}
|
||||||
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
|
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
|
||||||
limit={300}
|
limit={300}
|
||||||
@@ -290,7 +226,9 @@ export const JellyfinAlbumFilters = ({
|
|||||||
<MultiSelectWithInvalidData
|
<MultiSelectWithInvalidData
|
||||||
clearable
|
clearable
|
||||||
data={tagsQuery.data.boolTags}
|
data={tagsQuery.data.boolTags}
|
||||||
defaultValue={selectedTags}
|
defaultValue={
|
||||||
|
query._custom?.[tagsQuery.data.boolTags.join('|')] ?? undefined
|
||||||
|
}
|
||||||
label={t('common.tags', { postProcess: 'sentenceCase' })}
|
label={t('common.tags', { postProcess: 'sentenceCase' })}
|
||||||
onChange={handleTagFilter}
|
onChange={handleTagFilter}
|
||||||
searchable
|
searchable
|
||||||
|
|||||||
@@ -7,16 +7,12 @@ import {
|
|||||||
MultiSelectWithInvalidData,
|
MultiSelectWithInvalidData,
|
||||||
SelectWithInvalidData,
|
SelectWithInvalidData,
|
||||||
} from '/@/renderer/components/select-with-invalid-data';
|
} from '/@/renderer/components/select-with-invalid-data';
|
||||||
|
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
||||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||||
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
||||||
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
||||||
import {
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
AlbumListFilter,
|
import { NDSongQueryFields } from '/@/shared/api/navidrome.types';
|
||||||
useCurrentServer,
|
|
||||||
useListStoreActions,
|
|
||||||
useListStoreByKey,
|
|
||||||
} from '/@/renderer/store';
|
|
||||||
import { NDSongQueryFields } from '/@/shared/api/navidrome/navidrome-types';
|
|
||||||
import { hasFeature } from '/@/shared/api/utils';
|
import { hasFeature } from '/@/shared/api/utils';
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
@@ -28,7 +24,6 @@ import { Text } from '/@/shared/components/text/text';
|
|||||||
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
|
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
|
||||||
import {
|
import {
|
||||||
AlbumArtistListSort,
|
AlbumArtistListSort,
|
||||||
AlbumListQuery,
|
|
||||||
GenreListSort,
|
GenreListSort,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
@@ -36,24 +31,26 @@ import {
|
|||||||
import { ServerFeature } from '/@/shared/types/features-types';
|
import { ServerFeature } from '/@/shared/types/features-types';
|
||||||
|
|
||||||
interface NavidromeAlbumFiltersProps {
|
interface NavidromeAlbumFiltersProps {
|
||||||
customFilters?: Partial<AlbumListFilter>;
|
|
||||||
disableArtistFilter?: boolean;
|
disableArtistFilter?: boolean;
|
||||||
onFilterChange: (filters: AlbumListFilter) => void;
|
|
||||||
pageKey: string;
|
|
||||||
serverId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NavidromeAlbumFilters = ({
|
export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFiltersProps) => {
|
||||||
customFilters,
|
|
||||||
disableArtistFilter,
|
|
||||||
onFilterChange,
|
|
||||||
pageKey,
|
|
||||||
serverId,
|
|
||||||
}: NavidromeAlbumFiltersProps) => {
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { filter } = useListStoreByKey<AlbumListQuery>({ key: pageKey });
|
|
||||||
const { setFilter } = useListStoreActions();
|
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
const serverId = server.id;
|
||||||
|
|
||||||
|
const {
|
||||||
|
query,
|
||||||
|
setAlbumArtist,
|
||||||
|
setAlbumCompilation,
|
||||||
|
setAlbumFavorite,
|
||||||
|
setAlbumGenre,
|
||||||
|
setAlbumHasRating,
|
||||||
|
setAlbumRecentlyPlayed,
|
||||||
|
setCustom,
|
||||||
|
setMaxAlbumYear,
|
||||||
|
setMinAlbumYear,
|
||||||
|
} = useAlbumListFilters();
|
||||||
|
|
||||||
const genreListQuery = useQuery(
|
const genreListQuery = useQuery(
|
||||||
genresQueries.list({
|
genresQueries.list({
|
||||||
@@ -78,21 +75,6 @@ export const NavidromeAlbumFilters = ({
|
|||||||
}));
|
}));
|
||||||
}, [genreListQuery.data]);
|
}, [genreListQuery.data]);
|
||||||
|
|
||||||
const hasBrf = hasFeature(server, ServerFeature.BFR);
|
|
||||||
|
|
||||||
const handleGenresFilter = debounce((e: null | string[]) => {
|
|
||||||
const updatedFilters = setFilter({
|
|
||||||
customFilters,
|
|
||||||
data: {
|
|
||||||
_custom: filter._custom,
|
|
||||||
genres: e ? e : undefined,
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
}, 250);
|
|
||||||
|
|
||||||
const tagsQuery = useQuery(
|
const tagsQuery = useQuery(
|
||||||
sharedQueries.tags({
|
sharedQueries.tags({
|
||||||
options: {
|
options: {
|
||||||
@@ -110,34 +92,16 @@ export const NavidromeAlbumFilters = ({
|
|||||||
{
|
{
|
||||||
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||||
onChange: (favorite?: boolean) => {
|
onChange: (favorite?: boolean) => {
|
||||||
const updatedFilters = setFilter({
|
setAlbumFavorite(favorite ?? null);
|
||||||
customFilters,
|
|
||||||
data: {
|
|
||||||
_custom: filter._custom,
|
|
||||||
favorite,
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
},
|
},
|
||||||
value: filter.favorite,
|
value: query.favorite,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
|
label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
|
||||||
onChange: (compilation?: boolean) => {
|
onChange: (compilation?: boolean) => {
|
||||||
const updatedFilters = setFilter({
|
setAlbumCompilation(compilation ?? null);
|
||||||
customFilters,
|
|
||||||
data: {
|
|
||||||
_custom: filter._custom,
|
|
||||||
compilation,
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
},
|
},
|
||||||
value: filter.compilation,
|
value: query.compilation,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -145,63 +109,25 @@ export const NavidromeAlbumFilters = ({
|
|||||||
{
|
{
|
||||||
label: t('filter.isRated', { postProcess: 'sentenceCase' }),
|
label: t('filter.isRated', { postProcess: 'sentenceCase' }),
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const updatedFilters = setFilter({
|
const hasRating = e.currentTarget.checked ? true : undefined;
|
||||||
customFilters,
|
setAlbumHasRating(hasRating ?? null);
|
||||||
data: {
|
|
||||||
_custom: {
|
|
||||||
...filter._custom,
|
|
||||||
navidrome: {
|
|
||||||
...filter._custom?.navidrome,
|
|
||||||
has_rating: e.currentTarget.checked ? true : undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
},
|
},
|
||||||
value: filter._custom?.navidrome?.has_rating,
|
value: query.hasRating,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('filter.isRecentlyPlayed', { postProcess: 'sentenceCase' }),
|
label: t('filter.isRecentlyPlayed', { postProcess: 'sentenceCase' }),
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const updatedFilters = setFilter({
|
const recentlyPlayed = e.currentTarget.checked ? true : undefined;
|
||||||
customFilters,
|
setAlbumRecentlyPlayed(recentlyPlayed ?? null);
|
||||||
data: {
|
|
||||||
_custom: {
|
|
||||||
...filter._custom,
|
|
||||||
navidrome: {
|
|
||||||
...filter._custom?.navidrome,
|
|
||||||
recently_played: e.currentTarget.checked ? true : undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
},
|
},
|
||||||
value: filter._custom?.navidrome?.recently_played,
|
value: query.recentlyPlayed,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleYearFilter = debounce((e: number | string) => {
|
const handleYearFilter = debounce((e: number | string) => {
|
||||||
const updatedFilters = setFilter({
|
const year = e === '' ? undefined : (e as number);
|
||||||
customFilters,
|
setMinAlbumYear(year ?? null);
|
||||||
data: {
|
setMaxAlbumYear(year ?? null);
|
||||||
_custom: {
|
|
||||||
...filter._custom,
|
|
||||||
navidrome: {
|
|
||||||
...filter._custom?.navidrome,
|
|
||||||
year: e === '' ? undefined : (e as number),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
const albumArtistListQuery = useQuery(
|
const albumArtistListQuery = useQuery(
|
||||||
@@ -211,7 +137,6 @@ export const NavidromeAlbumFilters = ({
|
|||||||
staleTime: 1000 * 60 * 1,
|
staleTime: 1000 * 60 * 1,
|
||||||
},
|
},
|
||||||
query: {
|
query: {
|
||||||
// searchTerm: debouncedSearchTerm,
|
|
||||||
sortBy: AlbumArtistListSort.NAME,
|
sortBy: AlbumArtistListSort.NAME,
|
||||||
sortOrder: SortOrder.ASC,
|
sortOrder: SortOrder.ASC,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
@@ -229,85 +154,60 @@ export const NavidromeAlbumFilters = ({
|
|||||||
}));
|
}));
|
||||||
}, [albumArtistListQuery?.data?.items]);
|
}, [albumArtistListQuery?.data?.items]);
|
||||||
|
|
||||||
const handleAlbumArtistFilter = (e: null | string) => {
|
|
||||||
const updatedFilters = setFilter({
|
|
||||||
data: {
|
|
||||||
_custom: {
|
|
||||||
...filter._custom,
|
|
||||||
navidrome: {
|
|
||||||
...filter._custom?.navidrome,
|
|
||||||
artist_id: e || undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.ALBUM,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTagFilter = debounce((tag: string, e: null | string) => {
|
const handleTagFilter = debounce((tag: string, e: null | string) => {
|
||||||
const updatedFilters = setFilter({
|
setCustom((prev) => ({
|
||||||
customFilters,
|
...prev,
|
||||||
data: {
|
[tag]: e || undefined,
|
||||||
_custom: {
|
}));
|
||||||
...filter._custom,
|
|
||||||
navidrome: {
|
|
||||||
...filter._custom?.navidrome,
|
|
||||||
[tag]: e || undefined,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.SONG,
|
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
|
const hasBFR = hasFeature(server, ServerFeature.BFR);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack p="0.8rem">
|
<Stack p="0.8rem">
|
||||||
{yesNoUndefinedFilters.map((filter) => (
|
{yesNoUndefinedFilters.map((filter) => (
|
||||||
<Group justify="space-between" key={`nd-filter-${filter.label}`}>
|
<Group justify="space-between" key={`nd-filter-${filter.label}`}>
|
||||||
<Text>{filter.label}</Text>
|
<Text>{filter.label}</Text>
|
||||||
<YesNoSelect onChange={filter.onChange} size="xs" value={filter.value} />
|
<YesNoSelect
|
||||||
|
onChange={filter.onChange}
|
||||||
|
size="xs"
|
||||||
|
value={filter.value ?? undefined}
|
||||||
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
{toggleFilters.map((filter) => (
|
{toggleFilters.map((filter) => (
|
||||||
<Group justify="space-between" key={`nd-filter-${filter.label}`}>
|
<Group justify="space-between" key={`nd-filter-${filter.label}`}>
|
||||||
<Text>{filter.label}</Text>
|
<Text>{filter.label}</Text>
|
||||||
<Switch checked={filter?.value || false} onChange={filter.onChange} />
|
<Switch checked={filter?.value ?? false} onChange={filter.onChange} />
|
||||||
</Group>
|
</Group>
|
||||||
))}
|
))}
|
||||||
<Divider my="0.5rem" />
|
<Divider my="0.5rem" />
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
defaultValue={filter._custom?.navidrome?.year}
|
defaultValue={query.minYear ?? undefined}
|
||||||
hideControls={false}
|
hideControls={false}
|
||||||
label={t('common.year', { postProcess: 'titleCase' })}
|
label={t('common.year', { postProcess: 'titleCase' })}
|
||||||
max={5000}
|
max={5000}
|
||||||
min={0}
|
min={0}
|
||||||
onChange={(e) => handleYearFilter(e)}
|
onChange={(e) => handleYearFilter(e)}
|
||||||
/>
|
/>
|
||||||
{!hasBrf && (
|
<SelectWithInvalidData
|
||||||
<SelectWithInvalidData
|
clearable
|
||||||
clearable
|
data={genreList}
|
||||||
data={genreList}
|
defaultValue={query.genres ? query.genres[0] : undefined}
|
||||||
defaultValue={filter.genres && filter.genres[0]}
|
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
||||||
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
onChange={(e) => (e ? setAlbumGenre([e]) : undefined)}
|
||||||
onChange={(value) => handleGenresFilter(value !== null ? [value] : null)}
|
searchable
|
||||||
searchable
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Group>
|
</Group>
|
||||||
{hasBrf && (
|
{hasBFR && (
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<MultiSelectWithInvalidData
|
<MultiSelectWithInvalidData
|
||||||
clearable
|
clearable
|
||||||
data={genreList}
|
data={genreList}
|
||||||
defaultValue={filter.genres}
|
defaultValue={query.genres}
|
||||||
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
|
label={t('entity.genre', { count: 2, postProcess: 'sentenceCase' })}
|
||||||
onChange={handleGenresFilter}
|
onChange={(e) => (e ? setAlbumGenre(e) : undefined)}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
@@ -316,11 +216,11 @@ export const NavidromeAlbumFilters = ({
|
|||||||
<SelectWithInvalidData
|
<SelectWithInvalidData
|
||||||
clearable
|
clearable
|
||||||
data={selectableAlbumArtists}
|
data={selectableAlbumArtists}
|
||||||
defaultValue={filter._custom?.navidrome?.artist_id}
|
defaultValue={query.artistIds ? query.artistIds[0] : undefined}
|
||||||
disabled={disableArtistFilter}
|
disabled={disableArtistFilter}
|
||||||
label={t('entity.artist', { count: 1, postProcess: 'titleCase' })}
|
label={t('entity.artist', { count: 1, postProcess: 'titleCase' })}
|
||||||
limit={300}
|
limit={300}
|
||||||
onChange={handleAlbumArtistFilter}
|
onChange={(e) => setAlbumArtist(e ? [e] : null)}
|
||||||
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
|
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
|
||||||
searchable
|
searchable
|
||||||
/>
|
/>
|
||||||
@@ -332,9 +232,7 @@ export const NavidromeAlbumFilters = ({
|
|||||||
<SelectWithInvalidData
|
<SelectWithInvalidData
|
||||||
clearable
|
clearable
|
||||||
data={tag.options}
|
data={tag.options}
|
||||||
defaultValue={
|
defaultValue={query._custom?.[tag.name] as string | undefined}
|
||||||
filter._custom?.navidrome?.[tag.name] as string | undefined
|
|
||||||
}
|
|
||||||
label={
|
label={
|
||||||
NDSongQueryFields.find((i) => i.value === tag.name)?.label ||
|
NDSongQueryFields.find((i) => i.value === tag.name)?.label ||
|
||||||
tag.name
|
tag.name
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
|
import { parseAsArrayOf, parseAsBoolean, parseAsInteger, parseAsString, useQueryState } from 'nuqs';
|
||||||
import { ChangeEvent, useMemo, useState } from 'react';
|
import { ChangeEvent, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||||
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
||||||
import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
|
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||||
|
import { AlbumListFilter } from '/@/renderer/store';
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||||
@@ -15,30 +17,34 @@ import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
|||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
import { Switch } from '/@/shared/components/switch/switch';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import {
|
import { AlbumArtistListSort, GenreListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||||
AlbumArtistListSort,
|
|
||||||
AlbumListQuery,
|
|
||||||
GenreListSort,
|
|
||||||
LibraryItem,
|
|
||||||
SortOrder,
|
|
||||||
} from '/@/shared/types/domain-types';
|
|
||||||
|
|
||||||
interface SubsonicAlbumFiltersProps {
|
interface SubsonicAlbumFiltersProps {
|
||||||
disableArtistFilter?: boolean;
|
disableArtistFilter?: boolean;
|
||||||
onFilterChange: (filters: AlbumListFilter) => void;
|
onFilterChange: (filters: AlbumListFilter) => void;
|
||||||
pageKey: string;
|
|
||||||
serverId: string;
|
serverId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SubsonicAlbumFilters = ({
|
export const SubsonicAlbumFilters = ({
|
||||||
disableArtistFilter,
|
disableArtistFilter,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
pageKey,
|
|
||||||
serverId,
|
serverId,
|
||||||
}: SubsonicAlbumFiltersProps) => {
|
}: SubsonicAlbumFiltersProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { filter } = useListStoreByKey<AlbumListQuery>({ key: pageKey });
|
|
||||||
const { setFilter } = useListStoreActions();
|
const [favorite, setFavorite] = useQueryState(FILTER_KEYS.ALBUM.FAVORITE, parseAsBoolean);
|
||||||
|
|
||||||
|
const [minYear, setMinYear] = useQueryState(FILTER_KEYS.ALBUM.MIN_YEAR, parseAsInteger);
|
||||||
|
|
||||||
|
const [maxYear, setMaxYear] = useQueryState(FILTER_KEYS.ALBUM.MAX_YEAR, parseAsInteger);
|
||||||
|
|
||||||
|
const [genres, setGenres] = useQueryState(FILTER_KEYS.ALBUM.GENRES, parseAsString);
|
||||||
|
|
||||||
|
const [artistIds, setArtistIds] = useQueryState(
|
||||||
|
FILTER_KEYS.ALBUM.ARTIST_IDS,
|
||||||
|
parseAsArrayOf(parseAsString),
|
||||||
|
);
|
||||||
|
|
||||||
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
|
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
|
||||||
|
|
||||||
const albumArtistListQuery = useQuery(
|
const albumArtistListQuery = useQuery(
|
||||||
@@ -66,14 +72,11 @@ export const SubsonicAlbumFilters = ({
|
|||||||
}, [albumArtistListQuery?.data?.items]);
|
}, [albumArtistListQuery?.data?.items]);
|
||||||
|
|
||||||
const handleAlbumArtistFilter = (e: null | string[]) => {
|
const handleAlbumArtistFilter = (e: null | string[]) => {
|
||||||
const updatedFilters = setFilter({
|
setArtistIds(e ?? null);
|
||||||
data: {
|
const updatedFilters: Partial<AlbumListFilter> = {
|
||||||
artistIds: e?.length ? e : undefined,
|
artistIds: e?.length ? e : undefined,
|
||||||
},
|
};
|
||||||
itemType: LibraryItem.ALBUM,
|
onFilterChange(updatedFilters as AlbumListFilter);
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const genreListQuery = useQuery(
|
const genreListQuery = useQuery(
|
||||||
@@ -100,54 +103,41 @@ export const SubsonicAlbumFilters = ({
|
|||||||
}, [genreListQuery.data]);
|
}, [genreListQuery.data]);
|
||||||
|
|
||||||
const handleGenresFilter = debounce((e: null | string) => {
|
const handleGenresFilter = debounce((e: null | string) => {
|
||||||
const updatedFilters = setFilter({
|
setGenres(e ?? null);
|
||||||
data: {
|
const updatedFilters: Partial<AlbumListFilter> = {
|
||||||
genres: e ? [e] : undefined,
|
genres: e ? [e] : undefined,
|
||||||
},
|
};
|
||||||
itemType: LibraryItem.ALBUM,
|
onFilterChange(updatedFilters as AlbumListFilter);
|
||||||
key: pageKey,
|
|
||||||
}) as AlbumListFilter;
|
|
||||||
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
}, 250);
|
}, 250);
|
||||||
|
|
||||||
const toggleFilters = [
|
const toggleFilters = [
|
||||||
{
|
{
|
||||||
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
|
||||||
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
onChange: (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const updatedFilters = setFilter({
|
const favoriteValue = e.target.checked ? true : undefined;
|
||||||
data: {
|
setFavorite(favoriteValue ?? null);
|
||||||
favorite: e.target.checked ? true : undefined,
|
const updatedFilters: Partial<AlbumListFilter> = {
|
||||||
},
|
favorite: favoriteValue,
|
||||||
itemType: LibraryItem.ALBUM,
|
};
|
||||||
key: pageKey,
|
onFilterChange(updatedFilters as AlbumListFilter);
|
||||||
}) as AlbumListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
},
|
},
|
||||||
value: filter.favorite,
|
value: favorite,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleYearFilter = debounce((e: number | string, type: 'max' | 'min') => {
|
const handleYearFilter = debounce((e: number | string, type: 'max' | 'min') => {
|
||||||
let data: Partial<AlbumListQuery> = {};
|
const year = e ? Number(e) : undefined;
|
||||||
|
|
||||||
if (type === 'min') {
|
if (type === 'min') {
|
||||||
data = {
|
setMinYear(year ?? null);
|
||||||
minYear: e ? Number(e) : undefined,
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
data = {
|
setMaxYear(year ?? null);
|
||||||
maxYear: e ? Number(e) : undefined,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedFilters = setFilter({
|
const updatedFilters: Partial<AlbumListFilter> = {
|
||||||
data,
|
[type === 'min' ? 'minYear' : 'maxYear']: year,
|
||||||
itemType: LibraryItem.ALBUM,
|
};
|
||||||
key: pageKey,
|
onFilterChange(updatedFilters as AlbumListFilter);
|
||||||
}) as AlbumListFilter;
|
|
||||||
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -161,8 +151,8 @@ export const SubsonicAlbumFilters = ({
|
|||||||
<Divider my="0.5rem" />
|
<Divider my="0.5rem" />
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
defaultValue={filter.minYear}
|
defaultValue={minYear ?? undefined}
|
||||||
disabled={filter.genres?.length !== undefined}
|
disabled={genres !== null}
|
||||||
hideControls={false}
|
hideControls={false}
|
||||||
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
label={t('filter.fromYear', { postProcess: 'sentenceCase' })}
|
||||||
max={5000}
|
max={5000}
|
||||||
@@ -170,8 +160,8 @@ export const SubsonicAlbumFilters = ({
|
|||||||
onChange={(e) => handleYearFilter(e, 'min')}
|
onChange={(e) => handleYearFilter(e, 'min')}
|
||||||
/>
|
/>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
defaultValue={filter.maxYear}
|
defaultValue={maxYear ?? undefined}
|
||||||
disabled={filter.genres?.length !== undefined}
|
disabled={genres !== null}
|
||||||
hideControls={false}
|
hideControls={false}
|
||||||
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
label={t('filter.toYear', { postProcess: 'sentenceCase' })}
|
||||||
max={5000}
|
max={5000}
|
||||||
@@ -183,8 +173,8 @@ export const SubsonicAlbumFilters = ({
|
|||||||
<Select
|
<Select
|
||||||
clearable
|
clearable
|
||||||
data={genreList}
|
data={genreList}
|
||||||
defaultValue={filter.genres?.length ? filter.genres[0] : undefined}
|
defaultValue={genres ?? undefined}
|
||||||
disabled={Boolean(filter.minYear || filter.maxYear)}
|
disabled={Boolean(minYear || maxYear)}
|
||||||
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
label={t('entity.genre', { count: 1, postProcess: 'titleCase' })}
|
||||||
onChange={handleGenresFilter}
|
onChange={handleGenresFilter}
|
||||||
searchable
|
searchable
|
||||||
@@ -194,7 +184,7 @@ export const SubsonicAlbumFilters = ({
|
|||||||
<MultiSelectWithInvalidData
|
<MultiSelectWithInvalidData
|
||||||
clearable
|
clearable
|
||||||
data={selectableAlbumArtists}
|
data={selectableAlbumArtists}
|
||||||
defaultValue={filter?.artistIds}
|
defaultValue={artistIds ?? undefined}
|
||||||
disabled={disableArtistFilter}
|
disabled={disableArtistFilter}
|
||||||
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
|
label={t('entity.artist', { count: 2, postProcess: 'sentenceCase' })}
|
||||||
limit={300}
|
limit={300}
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import {
|
||||||
|
parseAsArrayOf,
|
||||||
|
parseAsBoolean,
|
||||||
|
parseAsInteger,
|
||||||
|
parseAsJson,
|
||||||
|
parseAsString,
|
||||||
|
useQueryState,
|
||||||
|
} from 'nuqs';
|
||||||
|
|
||||||
|
import { useMusicFolderIdFilter } from '/@/renderer/features/shared/hooks/use-music-folder-id-filter';
|
||||||
|
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
|
||||||
|
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
|
||||||
|
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
||||||
|
import { customFiltersSchema, FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||||
|
import { AlbumListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
export const useAlbumListFilters = () => {
|
||||||
|
const { sortBy } = useSortByFilter<AlbumListSort>(AlbumListSort.NAME);
|
||||||
|
|
||||||
|
const { sortOrder } = useSortOrderFilter(SortOrder.ASC);
|
||||||
|
|
||||||
|
const { musicFolderId } = useMusicFolderIdFilter('');
|
||||||
|
|
||||||
|
const { searchTerm, setSearchTerm } = useSearchTermFilter('');
|
||||||
|
|
||||||
|
const [albumGenre, setAlbumGenre] = useQueryState(
|
||||||
|
FILTER_KEYS.ALBUM.GENRES,
|
||||||
|
parseAsArrayOf(parseAsString),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [albumArtist, setAlbumArtist] = useQueryState(
|
||||||
|
FILTER_KEYS.ALBUM.ARTIST_IDS,
|
||||||
|
parseAsArrayOf(parseAsString),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [minAlbumYear, setMinAlbumYear] = useQueryState(
|
||||||
|
FILTER_KEYS.ALBUM.MIN_YEAR,
|
||||||
|
parseAsInteger,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [maxAlbumYear, setMaxAlbumYear] = useQueryState(
|
||||||
|
FILTER_KEYS.ALBUM.MAX_YEAR,
|
||||||
|
parseAsInteger,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [albumFavorite, setAlbumFavorite] = useQueryState(
|
||||||
|
FILTER_KEYS.ALBUM.FAVORITE,
|
||||||
|
parseAsBoolean,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [albumCompilation, setAlbumCompilation] = useQueryState(
|
||||||
|
FILTER_KEYS.ALBUM.COMPILATION,
|
||||||
|
parseAsBoolean,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [albumHasRating, setAlbumHasRating] = useQueryState(
|
||||||
|
FILTER_KEYS.ALBUM.HAS_RATING,
|
||||||
|
parseAsBoolean,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [albumRecentlyPlayed, setAlbumRecentlyPlayed] = useQueryState(
|
||||||
|
FILTER_KEYS.ALBUM.RECENTLY_PLAYED,
|
||||||
|
parseAsBoolean,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [custom, setCustom] = useQueryState(
|
||||||
|
FILTER_KEYS.ALBUM._CUSTOM,
|
||||||
|
parseAsJson(customFiltersSchema),
|
||||||
|
);
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
[FILTER_KEYS.ALBUM._CUSTOM]: custom ?? undefined,
|
||||||
|
[FILTER_KEYS.ALBUM.ARTIST_IDS]: albumArtist ?? undefined,
|
||||||
|
[FILTER_KEYS.ALBUM.COMPILATION]: albumCompilation ?? undefined,
|
||||||
|
[FILTER_KEYS.ALBUM.FAVORITE]: albumFavorite ?? undefined,
|
||||||
|
[FILTER_KEYS.ALBUM.GENRES]: albumGenre ?? undefined,
|
||||||
|
[FILTER_KEYS.ALBUM.HAS_RATING]: albumHasRating ?? undefined,
|
||||||
|
[FILTER_KEYS.ALBUM.MAX_YEAR]: maxAlbumYear ?? undefined,
|
||||||
|
[FILTER_KEYS.ALBUM.MIN_YEAR]: minAlbumYear ?? undefined,
|
||||||
|
[FILTER_KEYS.ALBUM.RECENTLY_PLAYED]: albumRecentlyPlayed ?? undefined,
|
||||||
|
[FILTER_KEYS.SHARED.MUSIC_FOLDER_ID]: musicFolderId ?? undefined,
|
||||||
|
[FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
|
||||||
|
[FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
|
||||||
|
[FILTER_KEYS.SHARED.SORT_ORDER]: sortOrder ?? undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
setAlbumArtist,
|
||||||
|
setAlbumCompilation,
|
||||||
|
setAlbumFavorite,
|
||||||
|
setAlbumGenre,
|
||||||
|
setAlbumHasRating,
|
||||||
|
setAlbumRecentlyPlayed,
|
||||||
|
setCustom,
|
||||||
|
setMaxAlbumYear,
|
||||||
|
setMinAlbumYear,
|
||||||
|
setSearchTerm,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,155 +1,31 @@
|
|||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import { useMemo, useState } from 'react';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import isEmpty from 'lodash/isEmpty';
|
|
||||||
import { useCallback, useMemo, useRef } from 'react';
|
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
|
|
||||||
import { ListContext } from '/@/renderer/context/list-context';
|
import { ListContext } from '/@/renderer/context/list-context';
|
||||||
import { albumQueries } from '/@/renderer/features/albums/api/album-api';
|
|
||||||
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
|
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
|
||||||
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
|
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
|
||||||
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
|
||||||
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
import { queryClient } from '/@/renderer/lib/react-query';
|
|
||||||
import { useCurrentServer, useListFilterByKey } from '/@/renderer/store';
|
|
||||||
import {
|
|
||||||
AlbumListQuery,
|
|
||||||
GenreListSort,
|
|
||||||
LibraryItem,
|
|
||||||
SortOrder,
|
|
||||||
} from '/@/shared/types/domain-types';
|
|
||||||
import { Play } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
const AlbumListRoute = () => {
|
const AlbumListRoute = () => {
|
||||||
const gridRef = useRef<null | VirtualInfiniteGridRef>(null);
|
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
|
||||||
const server = useCurrentServer();
|
|
||||||
const [searchParams] = useSearchParams();
|
|
||||||
const { albumArtistId, genreId } = useParams();
|
const { albumArtistId, genreId } = useParams();
|
||||||
const pageKey = albumArtistId ? `albumArtistAlbum` : 'album';
|
const pageKey = albumArtistId ? `albumArtistAlbum` : 'album';
|
||||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
|
||||||
|
|
||||||
const customFilters = useMemo(() => {
|
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
|
||||||
const value = {
|
|
||||||
...(albumArtistId && { artistIds: [albumArtistId] }),
|
|
||||||
...(genreId && {
|
|
||||||
genres: [genreId],
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (isEmpty(value)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}, [albumArtistId, genreId]);
|
|
||||||
|
|
||||||
const albumListFilter = useListFilterByKey<AlbumListQuery>({
|
|
||||||
filter: customFilters,
|
|
||||||
key: pageKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const genreList = useQuery(
|
|
||||||
genresQueries.list({
|
|
||||||
options: {
|
|
||||||
enabled: !!genreId,
|
|
||||||
gcTime: 1000 * 60 * 60,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
sortBy: GenreListSort.NAME,
|
|
||||||
sortOrder: SortOrder.ASC,
|
|
||||||
startIndex: 0,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const genreTitle = useMemo(() => {
|
|
||||||
if (!genreList.data) return '';
|
|
||||||
const genre = genreList.data.items.find((g) => g.id === genreId);
|
|
||||||
|
|
||||||
if (!genre) return 'Unknown';
|
|
||||||
|
|
||||||
return genre?.name;
|
|
||||||
}, [genreId, genreList.data]);
|
|
||||||
|
|
||||||
const itemCountCheck = useQuery(
|
|
||||||
albumQueries.listCount({
|
|
||||||
options: {
|
|
||||||
gcTime: 1000 * 60,
|
|
||||||
staleTime: 1000 * 60,
|
|
||||||
},
|
|
||||||
query: {
|
|
||||||
...albumListFilter,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
|
|
||||||
|
|
||||||
const handlePlay = useCallback(
|
|
||||||
async (args: { initialSongId?: string; playType: Play }) => {
|
|
||||||
if (!itemCount || itemCount === 0) return;
|
|
||||||
const { playType } = args;
|
|
||||||
const query = {
|
|
||||||
...albumListFilter,
|
|
||||||
...customFilters,
|
|
||||||
startIndex: 0,
|
|
||||||
};
|
|
||||||
const queryKey = queryKeys.albums.list(server?.id || '', query);
|
|
||||||
|
|
||||||
const albumListRes = await queryClient.fetchQuery({
|
|
||||||
queryFn: ({ signal }) => {
|
|
||||||
return api.controller.getAlbumList({
|
|
||||||
apiClientProps: { serverId: server?.id || '', signal },
|
|
||||||
query,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
queryKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
const albumIds = albumListRes?.items?.map((a) => a.id) || [];
|
|
||||||
|
|
||||||
handlePlayQueueAdd?.({
|
|
||||||
byItemType: {
|
|
||||||
id: albumIds,
|
|
||||||
type: LibraryItem.ALBUM,
|
|
||||||
},
|
|
||||||
playType,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[albumListFilter, customFilters, handlePlayQueueAdd, itemCount, server],
|
|
||||||
);
|
|
||||||
|
|
||||||
const providerValue = useMemo(() => {
|
const providerValue = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
customFilters,
|
|
||||||
handlePlay,
|
|
||||||
id: albumArtistId ?? genreId,
|
id: albumArtistId ?? genreId,
|
||||||
|
itemCount,
|
||||||
pageKey,
|
pageKey,
|
||||||
|
setItemCount,
|
||||||
};
|
};
|
||||||
}, [albumArtistId, customFilters, genreId, handlePlay, pageKey]);
|
}, [albumArtistId, genreId, itemCount, pageKey, setItemCount]);
|
||||||
|
|
||||||
const artist = searchParams.get('artistName');
|
|
||||||
const title = artist ? artist : genreId ? genreTitle : undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
<ListContext.Provider value={providerValue}>
|
<ListContext.Provider value={providerValue}>
|
||||||
<AlbumListHeader
|
<AlbumListHeader />
|
||||||
genreId={genreId}
|
<AlbumListContent />
|
||||||
gridRef={gridRef}
|
|
||||||
itemCount={itemCount}
|
|
||||||
tableRef={tableRef}
|
|
||||||
title={title}
|
|
||||||
/>
|
|
||||||
<AlbumListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
|
||||||
</ListContext.Provider>
|
</ListContext.Provider>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user