add artist list

This commit is contained in:
Kendall Garner 2025-04-23 23:27:06 -07:00
parent 14e9f6ac41
commit e84a4b20bc
No known key found for this signature in database
GPG key ID: 9355F387FE765C94
22 changed files with 1369 additions and 192 deletions

View file

@ -0,0 +1,47 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { lazy, MutableRefObject, Suspense } from 'react';
import { Spinner } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ListDisplayType } from '/@/renderer/types';
import { useListStoreByKey } from '../../../store/list.store';
import { useListContext } from '/@/renderer/context/list-context';
const ArtistListGridView = lazy(() =>
import('/@/renderer/features/artists/components/artist-list-grid-view').then((module) => ({
default: module.ArtistListGridView,
})),
);
const ArtistListTableView = lazy(() =>
import('/@/renderer/features/artists/components/artist-list-table-view').then((module) => ({
default: module.ArtistListTableView,
})),
);
interface ArtistListContentProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const ArtistListContent = ({ itemCount, gridRef, tableRef }: ArtistListContentProps) => {
const { pageKey } = useListContext();
const { display } = useListStoreByKey({ key: pageKey });
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER;
return (
<Suspense fallback={<Spinner container />}>
{isGrid ? (
<ArtistListGridView
gridRef={gridRef}
itemCount={itemCount}
/>
) : (
<ArtistListTableView
itemCount={itemCount}
tableRef={tableRef}
/>
)}
</Suspense>
);
};

View file

@ -0,0 +1,173 @@
import { QueryKey, useQueryClient } from '@tanstack/react-query';
import { MutableRefObject, useCallback, useMemo } from 'react';
import AutoSizer, { Size } from 'react-virtualized-auto-sizer';
import { ListOnScrollProps } from 'react-window';
import { VirtualGridAutoSizerContainer } from '../../../components/virtual-grid/virtual-grid-wrapper';
import { useListStoreByKey } from '../../../store/list.store';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import {
AlbumArtist,
ArtistListQuery,
ArtistListResponse,
ArtistListSort,
LibraryItem,
} from '/@/renderer/api/types';
import { ALBUMARTIST_CARD_ROWS } from '/@/renderer/components';
import { VirtualInfiniteGrid, VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { useListContext } from '/@/renderer/context/list-context';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useListStoreActions } from '/@/renderer/store';
import { CardRow, ListDisplayType } from '/@/renderer/types';
import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite';
interface ArtistListGridViewProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
itemCount?: number;
}
export const ArtistListGridView = ({ itemCount, gridRef }: ArtistListGridViewProps) => {
const queryClient = useQueryClient();
const server = useCurrentServer();
const handlePlayQueueAdd = usePlayQueueAdd();
const { pageKey } = useListContext();
const { grid, display, filter } = useListStoreByKey<ArtistListQuery>({ key: pageKey });
const { setGrid } = useListStoreActions();
const handleFavorite = useHandleFavorite({ gridRef, server });
const fetchInitialData = useCallback(() => {
const query: Omit<ArtistListQuery, 'startIndex' | 'limit'> = {
...filter,
};
const queriesFromCache: [QueryKey, ArtistListResponse][] = queryClient.getQueriesData({
exact: false,
fetchStatus: 'idle',
queryKey: queryKeys.artists.list(server?.id || '', query),
stale: false,
});
const itemData = [];
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;
}, [filter, queryClient, server?.id]);
const fetch = useCallback(
async ({ skip: startIndex, take: limit }: { skip: number; take: number }) => {
const query: ArtistListQuery = {
...filter,
limit,
startIndex,
};
const queryKey = queryKeys.artists.list(server?.id || '', query);
const artistsRes = await queryClient.fetchQuery(
queryKey,
async ({ signal }) =>
api.controller.getArtistList({
apiClientProps: {
server,
signal,
},
query,
}),
{ cacheTime: 1000 * 60 * 1 },
);
return artistsRes;
},
[filter, queryClient, server],
);
const handleGridScroll = useCallback(
(e: ListOnScrollProps) => {
setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey });
},
[pageKey, setGrid],
);
const cardRows = useMemo(() => {
const rows: CardRow<AlbumArtist>[] = [ALBUMARTIST_CARD_ROWS.name];
switch (filter.sortBy) {
case ArtistListSort.DURATION:
rows.push(ALBUMARTIST_CARD_ROWS.duration);
break;
case ArtistListSort.FAVORITED:
break;
case ArtistListSort.NAME:
break;
case ArtistListSort.ALBUM_COUNT:
rows.push(ALBUMARTIST_CARD_ROWS.albumCount);
break;
case ArtistListSort.PLAY_COUNT:
rows.push(ALBUMARTIST_CARD_ROWS.playCount);
break;
case ArtistListSort.RANDOM:
break;
case ArtistListSort.RATING:
rows.push(ALBUMARTIST_CARD_ROWS.rating);
break;
case ArtistListSort.RECENTLY_ADDED:
break;
case ArtistListSort.SONG_COUNT:
rows.push(ALBUMARTIST_CARD_ROWS.songCount);
break;
case ArtistListSort.RELEASE_DATE:
break;
}
return rows;
}, [filter.sortBy]);
return (
<VirtualGridAutoSizerContainer>
<AutoSizer>
{({ height, width }: Size) => (
<VirtualInfiniteGrid
ref={gridRef}
cardRows={cardRows}
display={display || ListDisplayType.CARD}
fetchFn={fetch}
fetchInitialData={fetchInitialData}
handleFavorite={handleFavorite}
handlePlayQueueAdd={handlePlayQueueAdd}
height={height}
initialScrollOffset={grid?.scrollOffset || 0}
itemCount={itemCount || 0}
itemGap={grid?.itemGap ?? 10}
itemSize={grid?.itemSize || 200}
itemType={LibraryItem.ALBUM_ARTIST}
loading={itemCount === undefined || itemCount === null}
minimumBatchSize={40}
route={{
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
}}
width={width}
onScroll={handleGridScroll}
/>
)}
</AutoSizer>
</VirtualGridAutoSizerContainer>
);
};

View file

@ -0,0 +1,619 @@
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react';
import { IDatasource } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Divider, Flex, Group, Stack } from '@mantine/core';
import { useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next';
import { RiFolder2Line, RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react-icons/ri';
import { useListContext } from '../../../context/list-context';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import {
ArtistListQuery,
ArtistListSort,
LibraryItem,
ServerType,
SortOrder,
} from '/@/renderer/api/types';
import {
Button,
DropdownMenu,
MultiSelect,
Select,
Slider,
Switch,
Text,
} from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks';
import {
ArtistListFilter,
useCurrentServer,
useListStoreActions,
useListStoreByKey,
} from '/@/renderer/store';
import { ListDisplayType, TableColumn } from '/@/renderer/types';
import i18n from '/@/i18n/i18n';
import { useRoles } from '/@/renderer/features/artists/queries/roles-query';
const FILTERS = {
jellyfin: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.album', { postProcess: 'titleCase' }),
value: ArtistListSort.ALBUM,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
value: ArtistListSort.DURATION,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: ArtistListSort.NAME,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: ArtistListSort.RANDOM,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: ArtistListSort.RECENTLY_ADDED,
},
],
navidrome: [
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
value: ArtistListSort.ALBUM_COUNT,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
value: ArtistListSort.FAVORITED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
value: ArtistListSort.PLAY_COUNT,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: ArtistListSort.NAME,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
value: ArtistListSort.RATING,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
value: ArtistListSort.SONG_COUNT,
},
],
subsonic: [
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }),
value: ArtistListSort.ALBUM_COUNT,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
value: ArtistListSort.FAVORITED,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: ArtistListSort.NAME,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
value: ArtistListSort.RATING,
},
],
};
interface ArtistListHeaderFiltersProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderFiltersProps) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const server = useCurrentServer();
const { pageKey } = useListContext();
const { display, table, grid, filter } = useListStoreByKey<ArtistListQuery>({
key: pageKey,
});
const { setFilter, setTable, setTablePagination, setDisplayType, setGrid } =
useListStoreActions();
const cq = useContainerQuery();
const roles = useRoles({
options: {
cacheTime: 1000 * 60 * 60 * 2,
staleTime: 1000 * 60 * 60 * 2,
},
query: {},
serverId: server?.id,
});
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER;
const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id });
const sortByLabel =
(server?.type &&
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filter.sortBy)
?.name) ||
t('common.unknown', { postProcess: 'titleCase' });
const handleItemSize = (e: number) => {
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
setTable({ data: { rowHeight: e }, key: pageKey });
} else {
setGrid({ data: { itemSize: e }, key: pageKey });
}
};
const handleItemGap = (e: number) => {
setGrid({ data: { itemGap: e }, key: pageKey });
};
const debouncedHandleItemSize = debounce(handleItemSize, 20);
const fetch = useCallback(
async (startIndex: number, limit: number, filters: ArtistListFilter) => {
const queryKey = queryKeys.artists.list(server?.id || '', {
limit,
startIndex,
...filters,
});
const albums = await queryClient.fetchQuery(
queryKey,
async ({ signal }) =>
api.controller.getArtistList({
apiClientProps: {
server,
signal,
},
query: {
limit,
startIndex,
...filters,
},
}),
{ cacheTime: 1000 * 60 * 1 },
);
return albums;
},
[queryClient, server],
);
const handleFilterChange = useCallback(
async (filters: ArtistListFilter) => {
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
const dataSource: IDatasource = {
getRows: async (params) => {
const limit = params.endRow - params.startRow;
const startIndex = params.startRow;
const queryKey = queryKeys.artists.list(server?.id || '', {
limit,
startIndex,
...filters,
});
const artistsRes = await queryClient.fetchQuery(
queryKey,
async ({ signal }) =>
api.controller.getArtistList({
apiClientProps: {
server,
signal,
},
query: {
limit,
startIndex,
...filters,
},
}),
{ cacheTime: 1000 * 60 * 1 },
);
params.successCallback(
artistsRes?.items || [],
artistsRes?.totalRecordCount || 0,
);
},
rowCount: undefined,
};
tableRef.current?.api.setDatasource(dataSource);
tableRef.current?.api.purgeInfiniteCache();
tableRef.current?.api.ensureIndexVisible(0, 'top');
if (display === ListDisplayType.TABLE_PAGINATED) {
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
}
} else {
gridRef.current?.scrollTo(0);
gridRef.current?.resetLoadMoreItemsCache();
// Refetching within the virtualized grid may be inconsistent due to it refetching
// using an outdated set of filters. To avoid this, we fetch using the updated filters
// and then set the grid's data here.
const data = await fetch(0, 200, filters);
if (!data?.items) return;
gridRef.current?.setItemData(data.items);
}
},
[display, tableRef, server, queryClient, setTablePagination, pageKey, gridRef, fetch],
);
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({
data: {
sortBy: e.currentTarget.value as ArtistListSort,
sortOrder: sortOrder || SortOrder.ASC,
},
itemType: LibraryItem.ARTIST,
key: pageKey,
}) as ArtistListFilter;
handleFilterChange(updatedFilters);
},
[handleFilterChange, pageKey, server?.type, setFilter],
);
const handleSetMusicFolder = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
if (!e.currentTarget?.value) return;
let updatedFilters = null;
if (e.currentTarget.value === String(filter.musicFolderId)) {
updatedFilters = setFilter({
data: { musicFolderId: undefined },
itemType: LibraryItem.ARTIST,
key: pageKey,
}) as ArtistListFilter;
} else {
updatedFilters = setFilter({
data: { musicFolderId: e.currentTarget.value },
itemType: LibraryItem.ARTIST,
key: pageKey,
}) as ArtistListFilter;
}
handleFilterChange(updatedFilters);
},
[filter.musicFolderId, handleFilterChange, setFilter, pageKey],
);
const handleToggleSortOrder = useCallback(() => {
const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
const updatedFilters = setFilter({
data: { sortOrder: newSortOrder },
itemType: LibraryItem.ARTIST,
key: pageKey,
}) as ArtistListFilter;
handleFilterChange(updatedFilters);
}, [filter.sortOrder, handleFilterChange, pageKey, setFilter]);
const handleSetViewType = useCallback(
(e: MouseEvent<HTMLButtonElement>) => {
if (!e.currentTarget?.value) return;
setDisplayType({ data: e.currentTarget.value as ListDisplayType, key: pageKey });
},
[pageKey, setDisplayType],
);
const handleTableColumns = (values: TableColumn[]) => {
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], 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 = (e: ChangeEvent<HTMLInputElement>) => {
setTable({ data: { autoFit: e.currentTarget.checked }, key: pageKey });
if (e.currentTarget.checked) {
tableRef.current?.api.sizeColumnsToFit();
}
};
const handleRefresh = useCallback(() => {
queryClient.invalidateQueries(queryKeys.artists.list(server?.id || ''));
handleFilterChange(filter);
}, [filter, handleFilterChange, queryClient, server?.id]);
const handleSetRole = useCallback(
(e: string | null) => {
const updatedFilters = setFilter({
data: {
role: e || '',
},
itemType: LibraryItem.ARTIST,
key: pageKey,
}) as ArtistListFilter;
handleFilterChange(updatedFilters);
},
[handleFilterChange, pageKey, setFilter],
);
return (
<Flex justify="space-between">
<Group
ref={cq.ref}
spacing="sm"
w="100%"
>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
fw="600"
size="md"
variant="subtle"
>
{sortByLabel}
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
{FILTERS[server?.type as keyof typeof FILTERS].map((f) => (
<DropdownMenu.Item
key={`filter-${f.name}`}
$isActive={f.value === filter.sortBy}
value={f.value}
onClick={handleSetSortBy}
>
{f.name}
</DropdownMenu.Item>
))}
</DropdownMenu.Dropdown>
</DropdownMenu>
<Divider orientation="vertical" />
<OrderToggleButton
sortOrder={filter.sortOrder}
onToggle={handleToggleSortOrder}
/>
{server?.type === ServerType.JELLYFIN && (
<>
<Divider orientation="vertical" />
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
fw="600"
size="md"
variant="subtle"
>
{cq.isMd ? 'Folder' : <RiFolder2Line size={15} />}
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
{musicFoldersQuery.data?.items.map((folder) => (
<DropdownMenu.Item
key={`musicFolder-${folder.id}`}
$isActive={filter.musicFolderId === folder.id}
value={folder.id}
onClick={handleSetMusicFolder}
>
{folder.name}
</DropdownMenu.Item>
))}
</DropdownMenu.Dropdown>
</DropdownMenu>
</>
)}
{roles.data?.length && (
<>
<Divider orientation="vertical" />
<Select
data={roles.data}
value={filter.role}
onChange={handleSetRole}
/>
</>
)}
<Divider orientation="vertical" />
<Button
compact
size="md"
tooltip={{ label: t('common.refresh', { postProcess: 'titleCase' }) }}
variant="subtle"
onClick={handleRefresh}
>
<RiRefreshLine size="1.3rem" />
</Button>
<Divider orientation="vertical" />
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
compact
size="md"
variant="subtle"
>
<RiMoreFill size={15} />
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item
icon={<RiRefreshLine />}
onClick={handleRefresh}
>
{t('common.refresh', {
postProcess: 'titleCase',
})}
</DropdownMenu.Item>
</DropdownMenu.Dropdown>
</DropdownMenu>
</Group>
<Group>
<DropdownMenu
position="bottom-end"
width={425}
>
<DropdownMenu.Target>
<Button
compact
size="md"
variant="subtle"
>
<RiSettings3Fill size="1.3rem" />
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Label>
{t('table.config.general.displayType', { postProcess: 'sentenceCase' })}
</DropdownMenu.Label>
<DropdownMenu.Item
$isActive={display === ListDisplayType.CARD}
value={ListDisplayType.CARD}
onClick={handleSetViewType}
>
{t('table.config.view.card', {
postProcess: 'sentenceCase',
})}
</DropdownMenu.Item>
<DropdownMenu.Item
$isActive={display === ListDisplayType.POSTER}
value={ListDisplayType.POSTER}
onClick={handleSetViewType}
>
{t('table.config.view.poster', {
postProcess: 'sentenceCase',
})}
</DropdownMenu.Item>
<DropdownMenu.Item
$isActive={display === ListDisplayType.TABLE}
value={ListDisplayType.TABLE}
onClick={handleSetViewType}
>
{t('table.config.view.table', {
postProcess: 'sentenceCase',
})}
</DropdownMenu.Item>
<DropdownMenu.Divider />
<DropdownMenu.Label>
{t('table.config.general.itemSize', { postProcess: 'sentenceCase' })}
</DropdownMenu.Label>
<DropdownMenu.Item closeMenuOnClick={false}>
{display === ListDisplayType.CARD ||
display === ListDisplayType.POSTER ? (
<Slider
defaultValue={grid?.itemSize}
max={300}
min={150}
onChange={debouncedHandleItemSize}
/>
) : (
<Slider
defaultValue={table.rowHeight}
max={100}
min={30}
onChange={debouncedHandleItemSize}
/>
)}
</DropdownMenu.Item>
{isGrid && (
<>
<DropdownMenu.Label>
{t('table.config.general.itemGap', {
postProcess: 'sentenceCase',
})}
</DropdownMenu.Label>
<DropdownMenu.Item closeMenuOnClick={false}>
<Slider
defaultValue={grid?.itemGap || 0}
max={30}
min={0}
onChangeEnd={handleItemGap}
/>
</DropdownMenu.Item>
</>
)}
{!isGrid && (
<>
<DropdownMenu.Label>
{t('table.config.general.tableColumns', {
postProcess: 'sentenceCase',
})}
</DropdownMenu.Label>
<DropdownMenu.Item
closeMenuOnClick={false}
component="div"
sx={{ cursor: 'default' }}
>
<Stack>
<MultiSelect
clearable
data={ALBUMARTIST_TABLE_COLUMNS}
defaultValue={table?.columns.map(
(column) => column.column,
)}
width={300}
onChange={handleTableColumns}
/>
<Group position="apart">
<Text>
{t('table.config.general.autoFitColumns', {
postProcess: 'sentenceCase',
})}
</Text>
<Switch
defaultChecked={table.autoFit}
onChange={handleAutoFitColumns}
/>
</Group>
</Stack>
</DropdownMenu.Item>
</>
)}
</DropdownMenu.Dropdown>
</DropdownMenu>
</Group>
</Flex>
);
};

View file

@ -0,0 +1,77 @@
import type { ChangeEvent, MutableRefObject } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Flex, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next';
import { FilterBar } from '../../shared/components/filter-bar';
import { ArtistListQuery, LibraryItem } from '/@/renderer/api/types';
import { PageHeader, SearchInput } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { LibraryHeaderBar } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks';
import { ArtistListFilter, useCurrentServer } from '/@/renderer/store';
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
import { ArtistListHeaderFilters } from '/@/renderer/features/artists/components/artist-list-header-filters';
interface ArtistListHeaderProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const ArtistListHeader = ({ itemCount, gridRef, tableRef }: ArtistListHeaderProps) => {
const { t } = useTranslation();
const server = useCurrentServer();
const cq = useContainerQuery();
const { filter, refresh, search } = useDisplayRefresh<ArtistListQuery>({
gridRef,
itemCount,
itemType: LibraryItem.ARTIST,
server,
tableRef,
});
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
const updatedFilters = search(e) as ArtistListFilter;
refresh(updatedFilters);
}, 500);
return (
<Stack
ref={cq.ref}
spacing={0}
>
<PageHeader backgroundColor="var(--titlebar-bg)">
<Flex
justify="space-between"
w="100%"
>
<LibraryHeaderBar>
<LibraryHeaderBar.Title>
{t('entity.artist_other', { postProcess: 'titleCase' })}
</LibraryHeaderBar.Title>
<LibraryHeaderBar.Badge
isLoading={itemCount === null || itemCount === undefined}
>
{itemCount}
</LibraryHeaderBar.Badge>
</LibraryHeaderBar>
<Group>
<SearchInput
defaultValue={filter.searchTerm}
openedWidth={cq.isMd ? 250 : cq.isSm ? 200 : 150}
onChange={handleSearch}
/>
</Group>
</Flex>
</PageHeader>
<FilterBar>
<ArtistListHeaderFilters
gridRef={gridRef}
tableRef={tableRef}
/>
</FilterBar>
</Stack>
);
};

View file

@ -0,0 +1,40 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { MutableRefObject } from 'react';
import { useListContext } from '../../../context/list-context';
import { ARTIST_CONTEXT_MENU_ITEMS } from '../../context-menu/context-menu-items';
import { LibraryItem } from '/@/renderer/api/types';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
import { VirtualTable } from '/@/renderer/components/virtual-table';
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
import { useCurrentServer } from '/@/renderer/store';
interface ArtistListTableViewProps {
itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const ArtistListTableView = ({ itemCount, tableRef }: ArtistListTableViewProps) => {
const server = useCurrentServer();
const { pageKey } = useListContext();
const tableProps = useVirtualTable({
contextMenu: ARTIST_CONTEXT_MENU_ITEMS,
itemCount,
itemType: LibraryItem.ARTIST,
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>
);
};