add new artist list

This commit is contained in:
jeffvli
2025-11-14 00:11:31 -08:00
parent 164332d752
commit 8a8de4a1b6
18 changed files with 808 additions and 556 deletions
+13
View File
@@ -144,6 +144,19 @@ export const queryKeys: Record<
[serverId, 'albums', 'songs', query] as const,
},
artists: {
count: (serverId: string, query?: ArtistListQuery) => {
const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination) {
return [serverId, 'artists', 'count', filter, pagination] as const;
}
if (query) {
return [serverId, 'artists', 'count', filter] as const;
}
return [serverId, 'artists', 'count'] as const;
},
list: (serverId: string, query?: ArtistListQuery) => {
const { filter, pagination } = splitPaginatedQuery(query);
if (query && pagination) {
@@ -287,6 +287,21 @@ export const PLAYLIST_TABLE_COLUMNS = [
},
];
export const ARTIST_TABLE_COLUMNS = [
{
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
value: TableColumn.ROW_INDEX,
},
{
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
value: TableColumn.TITLE,
},
{
label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }),
value: TableColumn.ACTIONS,
},
];
export const GENRE_TABLE_COLUMNS = [
{
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
@@ -51,15 +51,29 @@ export const artistsQueries = {
...args.options,
});
},
artistListCount: (args: QueryHookArgs<ListCountQuery<ArtistListQuery>>) => {
artistList: (args: QueryHookArgs<ArtistListQuery>) => {
return queryOptions({
queryFn: ({ signal }) => {
return api.controller.getArtistListCount({
return api.controller.getArtistList({
apiClientProps: { serverId: args.serverId, signal },
query: args.query,
});
},
queryKey: queryKeys.albumArtists.count(
queryKey: queryKeys.artists.list(args.serverId, args.query),
...args.options,
});
},
artistListCount: (args: QueryHookArgs<ListCountQuery<ArtistListQuery>>) => {
return queryOptions({
queryFn: ({ signal }) => {
return api.controller
.getArtistList({
apiClientProps: { serverId: args.serverId, signal },
query: { ...args.query, limit: 1, startIndex: 0 },
})
.then((result) => result?.totalRecordCount ?? 0);
},
queryKey: queryKeys.artists.count(
args.serverId,
Object.keys(args.query).length === 0 ? undefined : args.query,
),
@@ -68,6 +68,7 @@ export const AlbumArtistListInfiniteTable = forwardRef<any, AlbumArtistListInfin
columns={columns}
data={data}
enableAlternateRowColors={enableAlternateRowColors}
enableExpansion={false}
enableHorizontalBorders={enableHorizontalBorders}
enableRowHoverHighlight={enableRowHoverHighlight}
enableSelection={enableSelection}
@@ -79,6 +79,7 @@ export const AlbumArtistListPaginatedTable = forwardRef<any, AlbumArtistListPagi
currentPage={currentPage}
data={data || []}
enableAlternateRowColors={enableAlternateRowColors}
enableExpansion={false}
enableHorizontalBorders={enableHorizontalBorders}
enableRowHoverHighlight={enableRowHoverHighlight}
enableSelection={enableSelection}
@@ -1,43 +1,129 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { lazy, Suspense } from 'react';
import { lazy, MutableRefObject, Suspense } from 'react';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
import { useListContext } from '/@/renderer/context/list-context';
import { useListStoreByKey } from '/@/renderer/store';
import { useArtistListFilters } from '/@/renderer/features/artists/hooks/use-artist-list-filters';
import { ItemListSettings, useCurrentServer, useListSettings } from '/@/renderer/store';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { ListDisplayType } from '/@/shared/types/types';
import { ItemListKey, ListDisplayType, ListPaginationType } from '/@/shared/types/types';
const ArtistListGridView = lazy(() =>
import('/@/renderer/features/artists/components/artist-list-grid-view').then((module) => ({
default: module.ArtistListGridView,
const ArtistListInfiniteGrid = lazy(() =>
import('/@/renderer/features/artists/components/artist-list-infinite-grid').then((module) => ({
default: module.ArtistListInfiniteGrid,
})),
);
const ArtistListTableView = lazy(() =>
import('/@/renderer/features/artists/components/artist-list-table-view').then((module) => ({
default: module.ArtistListTableView,
const ArtistListPaginatedGrid = lazy(() =>
import('/@/renderer/features/artists/components/artist-list-paginated-grid').then((module) => ({
default: module.ArtistListPaginatedGrid,
})),
);
interface ArtistListContentProps {
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>;
}
const ArtistListInfiniteTable = lazy(() =>
import('/@/renderer/features/artists/components/artist-list-infinite-table').then((module) => ({
default: module.ArtistListInfiniteTable,
})),
);
export const ArtistListContent = ({ gridRef, itemCount, tableRef }: ArtistListContentProps) => {
const { pageKey } = useListContext();
const { display } = useListStoreByKey({ key: pageKey });
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.GRID;
const ArtistListPaginatedTable = lazy(() =>
import('/@/renderer/features/artists/components/artist-list-paginated-table').then((module) => ({
default: module.ArtistListPaginatedTable,
})),
);
export const ArtistListContent = () => {
const { display, grid, itemsPerPage, pagination, table } = useListSettings(ItemListKey.ARTIST);
return (
<Suspense fallback={<Spinner container />}>
{isGrid ? (
<ArtistListGridView gridRef={gridRef} itemCount={itemCount} />
) : (
<ArtistListTableView itemCount={itemCount} tableRef={tableRef} />
)}
<ArtistListView
display={display}
grid={grid}
itemsPerPage={itemsPerPage}
pagination={pagination}
table={table}
/>
</Suspense>
);
};
export const ArtistListView = ({
display,
grid,
itemsPerPage,
pagination,
table,
}: ItemListSettings) => {
const server = useCurrentServer();
const { query } = useArtistListFilters();
switch (display) {
case ListDisplayType.GRID: {
switch (pagination) {
case ListPaginationType.INFINITE: {
return (
<ArtistListInfiniteGrid
gap={grid.itemGap}
itemsPerPage={itemsPerPage}
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
query={query}
serverId={server.id}
/>
);
}
case ListPaginationType.PAGINATED: {
return (
<ArtistListPaginatedGrid
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 (
<ArtistListInfiniteTable
autoFitColumns={table.autoFitColumns}
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 (
<ArtistListPaginatedTable
autoFitColumns={table.autoFitColumns}
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,474 +1,58 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQuery } from '@tanstack/react-query';
import { IDatasource } from '@ag-grid-community/core';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce';
import { MouseEvent, MutableRefObject, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import i18n from '/@/i18n/i18n';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { useListContext } from '/@/renderer/context/list-context';
import { ARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
import { OrderToggleButton } from '/@/renderer/features/shared/components/order-toggle-button';
import { RefreshButton } from '/@/renderer/features/shared/components/refresh-button';
import { ListMusicFolderDropdown } from '/@/renderer/features/shared/components/list-music-folder-dropdown';
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
import { ListSelectFilter } from '/@/renderer/features/shared/components/list-select-filter';
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { useContainerQuery } from '/@/renderer/hooks';
import {
ArtistListFilter,
PersistedTableColumn,
useCurrentServer,
useListStoreActions,
useListStoreByKey,
} from '/@/renderer/store';
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Button } from '/@/shared/components/button/button';
import { useCurrentServer } from '/@/renderer/store';
import { Divider } from '/@/shared/components/divider/divider';
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group';
import { Icon } from '/@/shared/components/icon/icon';
import { Select } from '/@/shared/components/select/select';
import {
ArtistListQuery,
ArtistListSort,
LibraryItem,
ServerType,
SortOrder,
} from '/@/shared/types/domain-types';
import { ListDisplayType } from '/@/shared/types/types';
import { ArtistListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
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<null | VirtualInfiniteGridRef>;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderFiltersProps) => {
const { t } = useTranslation();
const queryClient = useQueryClient();
const server = useCurrentServer();
const { pageKey } = useListContext();
const { display, filter, grid, table } = useListStoreByKey<ArtistListQuery>({
key: pageKey,
});
const { setDisplayType, setFilter, setGrid, setTable, setTablePagination } =
useListStoreActions();
export const ArtistListHeaderFilters = () => {
const cq = useContainerQuery();
const roles = useQuery(
sharedQueries.roles({
options: {
gcTime: 1000 * 60 * 60 * 2,
staleTime: 1000 * 60 * 60 * 2,
},
query: {},
serverId: server?.id,
}),
);
const server = useCurrentServer();
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.GRID;
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) ||
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({
gcTime: 1000 * 60 * 1,
queryFn: async ({ signal }) =>
api.controller.getArtistList({
apiClientProps: {
serverId: server?.id || '',
signal,
},
query: {
limit,
startIndex,
...filters,
},
}),
queryKey,
});
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({
gcTime: 1000 * 60 * 1,
queryFn: async ({ signal }) =>
api.controller.getArtistList({
apiClientProps: {
serverId: server?.id || '',
signal,
},
query: {
limit,
startIndex,
...filters,
},
}),
queryKey,
});
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: ArtistListFilter | null = 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(
(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],
width: 100,
} as PersistedTableColumn;
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 handleRefresh = useCallback(() => {
queryClient.invalidateQueries({ queryKey: queryKeys.artists.list(server?.id || '') });
handleFilterChange(filter);
}, [filter, handleFilterChange, queryClient, server?.id]);
const handleSetRole = useCallback(
(e: null | string) => {
const updatedFilters = setFilter({
data: {
role: e || '',
},
itemType: LibraryItem.ARTIST,
key: pageKey,
}) as ArtistListFilter;
handleFilterChange(updatedFilters);
},
[handleFilterChange, pageKey, setFilter],
);
const rolesQuery = useQuery(sharedQueries.roles({ query: {}, serverId: server.id }));
return (
<Flex justify="space-between">
<Group gap="sm" ref={cq.ref} w="100%">
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button variant="subtle">{sortByLabel}</Button>
</DropdownMenu.Target>
<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>
<ListSortByDropdown
defaultSortByValue={ArtistListSort.NAME}
itemType={LibraryItem.ARTIST}
listKey={ItemListKey.ARTIST}
/>
<Divider orientation="vertical" />
<OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} />
{server?.type === ServerType.JELLYFIN && (
<ListSortOrderToggleButton
defaultSortOrder={SortOrder.ASC}
listKey={ItemListKey.ARTIST}
/>
<ListMusicFolderDropdown listKey={ItemListKey.ARTIST} />
{rolesQuery.data && rolesQuery.data.length > 0 && (
<>
<Divider orientation="vertical" />
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<ActionIcon icon="folder" variant="subtle" />
</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>
<ListSelectFilter
data={rolesQuery.data}
filterKey={FILTER_KEYS.ARTIST.ROLE}
listKey={ItemListKey.ARTIST}
/>
</>
)}
{roles.data?.length && (
<>
<Select data={roles.data} onChange={handleSetRole} value={filter.role} />
</>
)}
<RefreshButton onClick={handleRefresh} />
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<MoreButton />
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<DropdownMenu.Item
leftSection={<Icon icon="refresh" />}
onClick={handleRefresh}
>
{t('common.refresh', {
postProcess: 'titleCase',
})}
</DropdownMenu.Item>
</DropdownMenu.Dropdown>
</DropdownMenu>
<ListRefreshButton listKey={ItemListKey.ARTIST} />
</Group>
<Group gap="xs" wrap="nowrap">
<Group gap="sm" wrap="nowrap">
<ListConfigMenu
autoFitColumns={table.autoFit}
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={ALBUMARTIST_TABLE_COLUMNS}
listKey={ItemListKey.ARTIST}
tableColumnsData={ARTIST_TABLE_COLUMNS}
/>
</Group>
</Flex>
@@ -1,68 +1,38 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import type { ChangeEvent, MutableRefObject } from 'react';
import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next';
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 { ArtistListHeaderFilters } from '/@/renderer/features/artists/components/artist-list-header-filters';
import { FilterBar } from '/@/renderer/features/shared/components/filter-bar';
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
import { useContainerQuery } from '/@/renderer/hooks';
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
import { ArtistListFilter, useCurrentServer } from '/@/renderer/store';
import { Flex } from '/@/shared/components/flex/flex';
import { ListSearchInput } from '/@/renderer/features/shared/components/list-search-input';
import { Group } from '/@/shared/components/group/group';
import { Stack } from '/@/shared/components/stack/stack';
import { ArtistListQuery, LibraryItem } from '/@/shared/types/domain-types';
interface ArtistListHeaderProps {
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>;
title?: string;
}
export const ArtistListHeader = ({ gridRef, itemCount, tableRef }: ArtistListHeaderProps) => {
export const ArtistListHeader = ({ title }: 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);
const { itemCount } = useListContext();
const pageTitle = title || t('entity.artist_other', { postProcess: 'titleCase' });
return (
<Stack gap={0} ref={cq.ref}>
<PageHeader>
<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} onChange={handleSearch} />
</Group>
</Flex>
<Stack gap={0}>
<PageHeader backgroundColor="var(--theme-colors-background)">
<LibraryHeaderBar>
<LibraryHeaderBar.PlayButton />
<LibraryHeaderBar.Title>{pageTitle}</LibraryHeaderBar.Title>
<LibraryHeaderBar.Badge isLoading={!itemCount}>{itemCount}</LibraryHeaderBar.Badge>
</LibraryHeaderBar>
<Group>
<ListSearchInput />
</Group>
</PageHeader>
<FilterBar>
<ArtistListHeaderFilters gridRef={gridRef} tableRef={tableRef} />
<ArtistListHeaderFilters />
</FilterBar>
</Stack>
);
@@ -0,0 +1,68 @@
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 { ItemGridList } from '/@/renderer/components/item-list/item-grid-list/item-grid-list';
import { ItemListGridComponentProps } from '/@/renderer/components/item-list/types';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import {
ArtistListQuery,
ArtistListSort,
LibraryItem,
SortOrder,
} from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
interface ArtistListInfiniteGridProps extends ItemListGridComponentProps<ArtistListQuery> {}
export const ArtistListInfiniteGrid = forwardRef<any, ArtistListInfiniteGridProps>(
(
{
gap = 'md',
itemsPerPage = 100,
itemsPerRow,
query = {
sortBy: ArtistListSort.NAME,
sortOrder: SortOrder.ASC,
},
saveScrollOffset = true,
serverId,
},
ref,
) => {
const listCountQuery = artistsQueries.artistListCount({
query: { ...query },
serverId: serverId,
}) as UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
const listQueryFn = api.controller.getArtistList;
const { data, onRangeChanged } = useItemListInfiniteLoader({
eventKey: ItemListKey.ARTIST,
itemsPerPage,
itemType: LibraryItem.ARTIST,
listCountQuery,
listQueryFn,
query,
serverId,
});
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
enabled: saveScrollOffset,
});
return (
<ItemGridList
data={data}
gap={gap}
initialTop={scrollOffset ?? 0}
itemsPerRow={itemsPerRow}
itemType={LibraryItem.ARTIST}
onRangeChanged={onRangeChanged}
onScrollEnd={handleOnScrollEnd}
/>
);
},
);
@@ -0,0 +1,87 @@
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 { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import {
ArtistListQuery,
ArtistListSort,
LibraryItem,
SortOrder,
} from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
interface ArtistListInfiniteTableProps extends ItemListTableComponentProps<ArtistListQuery> {}
export const ArtistListInfiniteTable = forwardRef<any, ArtistListInfiniteTableProps>(
(
{
autoFitColumns = false,
columns,
enableAlternateRowColors = false,
enableHorizontalBorders = false,
enableRowHoverHighlight = true,
enableSelection = true,
enableVerticalBorders = false,
itemsPerPage = 100,
query = {
sortBy: ArtistListSort.NAME,
sortOrder: SortOrder.ASC,
},
saveScrollOffset = true,
serverId,
size = 'default',
},
ref,
) => {
const listCountQuery = artistsQueries.artistListCount({
query: { ...query },
serverId: serverId,
}) as UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
const listQueryFn = api.controller.getArtistList;
const { data, onRangeChanged } = useItemListInfiniteLoader({
eventKey: ItemListKey.ARTIST,
itemsPerPage,
itemType: LibraryItem.ARTIST,
listCountQuery,
listQueryFn,
query,
serverId,
});
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
enabled: saveScrollOffset,
});
return (
<ItemTableList
autoFitColumns={autoFitColumns}
CellComponent={ItemTableListColumn}
columns={columns}
data={data}
enableAlternateRowColors={enableAlternateRowColors}
enableExpansion={false}
enableHorizontalBorders={enableHorizontalBorders}
enableRowHoverHighlight={enableRowHoverHighlight}
enableSelection={enableSelection}
enableVerticalBorders={enableVerticalBorders}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
}}
itemType={LibraryItem.ARTIST}
onRangeChanged={onRangeChanged}
onScrollEnd={handleOnScrollEnd}
ref={ref}
size={size}
/>
);
},
);
@@ -0,0 +1,83 @@
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 { 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 { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination';
import { ItemListGridComponentProps } from '/@/renderer/components/item-list/types';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import {
ArtistListQuery,
ArtistListSort,
LibraryItem,
SortOrder,
} from '/@/shared/types/domain-types';
interface ArtistListPaginatedGridProps extends ItemListGridComponentProps<ArtistListQuery> {}
export const ArtistListPaginatedGrid = forwardRef<any, ArtistListPaginatedGridProps>(
(
{
gap = 'md',
itemsPerPage = 100,
itemsPerRow,
query = {
sortBy: ArtistListSort.NAME,
sortOrder: SortOrder.ASC,
},
saveScrollOffset = true,
serverId,
},
ref,
) => {
const listCountQuery = artistsQueries.artistListCount({
query: { ...query },
serverId: serverId,
}) as UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
const listQueryFn = api.controller.getArtistList;
const { currentPage, onChange } = useItemListPagination();
const { data, pageCount, totalItemCount } = useItemListPaginatedLoader({
currentPage,
itemsPerPage,
itemType: LibraryItem.ARTIST,
listCountQuery,
listQueryFn,
query,
serverId,
});
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
enabled: saveScrollOffset,
});
return (
<ItemListWithPagination
currentPage={currentPage}
itemsPerPage={itemsPerPage}
onChange={onChange}
pageCount={pageCount}
totalItemCount={totalItemCount}
>
<ItemGridList
currentPage={currentPage}
data={data || []}
gap={gap}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
}}
itemsPerRow={itemsPerRow}
itemType={LibraryItem.ARTIST}
onScrollEnd={handleOnScrollEnd}
ref={ref}
/>
</ItemListWithPagination>
);
},
);
@@ -0,0 +1,98 @@
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 { 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 { 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 { ItemListTableComponentProps } from '/@/renderer/components/item-list/types';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import {
ArtistListQuery,
ArtistListSort,
LibraryItem,
SortOrder,
} from '/@/shared/types/domain-types';
interface ArtistListPaginatedTableProps extends ItemListTableComponentProps<ArtistListQuery> {}
export const ArtistListPaginatedTable = forwardRef<any, ArtistListPaginatedTableProps>(
(
{
autoFitColumns = false,
columns,
enableAlternateRowColors = false,
enableHorizontalBorders = false,
enableRowHoverHighlight = true,
enableSelection = true,
enableVerticalBorders = false,
itemsPerPage = 100,
query = {
sortBy: ArtistListSort.NAME,
sortOrder: SortOrder.ASC,
},
saveScrollOffset = true,
serverId,
size = 'default',
},
ref,
) => {
const listCountQuery = artistsQueries.artistListCount({
query: { ...query },
serverId: serverId,
}) as UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
const listQueryFn = api.controller.getArtistList;
const { currentPage, onChange } = useItemListPagination();
const { data, pageCount, totalItemCount } = useItemListPaginatedLoader({
currentPage,
itemsPerPage,
itemType: LibraryItem.ARTIST,
listCountQuery,
listQueryFn,
query,
serverId,
});
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
enabled: saveScrollOffset,
});
return (
<ItemListWithPagination
currentPage={currentPage}
itemsPerPage={itemsPerPage}
onChange={onChange}
pageCount={pageCount}
totalItemCount={totalItemCount}
>
<ItemTableList
autoFitColumns={autoFitColumns}
CellComponent={ItemTableListColumn}
columns={columns}
currentPage={currentPage}
data={data || []}
enableAlternateRowColors={enableAlternateRowColors}
enableExpansion={false}
enableHorizontalBorders={enableHorizontalBorders}
enableRowHoverHighlight={enableRowHoverHighlight}
enableSelection={enableSelection}
enableVerticalBorders={enableVerticalBorders}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
}}
itemType={LibraryItem.ARTIST}
onScrollEnd={handleOnScrollEnd}
ref={ref}
size={size}
/>
</ItemListWithPagination>
);
},
);
@@ -0,0 +1,33 @@
import { useMusicFolderIdFilter } from '/@/renderer/features/shared/hooks/use-music-folder-id-filter';
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
import { useSelectFilter } from '/@/renderer/features/shared/hooks/use-select-filter';
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { ArtistListSort } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
export const useArtistListFilters = () => {
const { sortBy } = useSortByFilter<ArtistListSort>(null, ItemListKey.ARTIST);
const { sortOrder } = useSortOrderFilter(null, ItemListKey.ARTIST);
const { musicFolderId } = useMusicFolderIdFilter(null, ItemListKey.ARTIST);
const { searchTerm, setSearchTerm } = useSearchTermFilter('');
const { value: role } = useSelectFilter(FILTER_KEYS.ARTIST.ROLE, '', ItemListKey.ARTIST);
const query = {
[FILTER_KEYS.ARTIST.ROLE]: role ?? 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,
setSearchTerm,
};
};
@@ -1,50 +1,30 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react';
import { useMemo, useState } from 'react';
import { ListContext } from '/@/renderer/context/list-context';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { ArtistListContent } from '/@/renderer/features/artists/components/artist-list-content';
import { ArtistListHeader } from '/@/renderer/features/artists/components/artist-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { useCurrentServer } from '/@/renderer/store/auth.store';
import { useListFilterByKey } from '/@/renderer/store/list.store';
import { ArtistListQuery, LibraryItem } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
const ArtistListRoute = () => {
const gridRef = useRef<null>(null);
const tableRef = useRef<AgGridReactType | null>(null);
const pageKey = LibraryItem.ARTIST;
const server = useCurrentServer();
const pageKey = ItemListKey.ARTIST;
const artistListFilter = useListFilterByKey<ArtistListQuery>({ key: pageKey });
const itemCountCheck = useQuery(
artistsQueries.artistListCount({
options: {
gcTime: 1000 * 60,
staleTime: 1000 * 60,
},
query: artistListFilter,
serverId: server?.id,
}),
);
const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data;
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
const providerValue = useMemo(() => {
return {
id: undefined,
itemCount,
pageKey,
setItemCount,
};
}, [pageKey]);
}, [itemCount, pageKey, setItemCount]);
return (
<AnimatedPage>
<ListContext.Provider value={providerValue}>
<ArtistListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
{/* <ArtistListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> */}
<ArtistListHeader />
<ArtistListContent />
</ListContext.Provider>
</AnimatedPage>
);
@@ -0,0 +1,78 @@
import { useSelectFilter } from '/@/renderer/features/shared/hooks/use-select-filter';
import { Button } from '/@/shared/components/button/button';
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
import { Select } from '/@/shared/components/select/select';
import { ItemListKey } from '/@/shared/types/types';
export type SelectOption = string | { label: string; value: string };
interface ListSelectFilterProps {
data?: Array<SelectOption>;
filterKey: string;
listKey: ItemListKey;
}
export const ListSelectFilter = ({ data, filterKey, listKey }: ListSelectFilterProps) => {
const selectData = data || [];
const { setValue, value } = useSelectFilter(filterKey, '', listKey);
const handleSetValue = (newValue: string) => {
if (newValue === value) {
setValue('');
return;
}
setValue(newValue);
};
const getOptionLabel = (option: SelectOption): string => {
if (typeof option === 'string') {
return option;
}
return option.label;
};
const getOptionValue = (option: SelectOption): string => {
if (typeof option === 'string') {
return option;
}
return option.value;
};
const selectedOption = selectData.find((option) => getOptionValue(option) === value);
const selectedLabel = selectedOption ? getOptionLabel(selectedOption) : '—';
return (
<Select
data={selectData}
onChange={(value) => handleSetValue(value ?? '')}
value={value ?? ''}
/>
);
return (
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button variant="subtle">{selectedLabel}</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
{selectData.map((option) => {
const optionValue = getOptionValue(option);
const optionLabel = getOptionLabel(option);
return (
<DropdownMenu.Item
isSelected={value === optionValue}
key={`${filterKey}-${optionValue}`}
onClick={() => handleSetValue(optionValue)}
value={optionValue}
>
{optionLabel}
</DropdownMenu.Item>
);
})}
</DropdownMenu.Dropdown>
</DropdownMenu>
);
};
@@ -6,6 +6,7 @@ import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
import {
AlbumArtistListSort,
AlbumListSort,
ArtistListSort,
GenreListSort,
LibraryItem,
ServerType,
@@ -445,6 +446,92 @@ const ALBUM_ARTIST_LIST_FILTERS: Partial<
],
};
const ARTIST_LIST_FILTERS: Partial<
Record<ServerType, Array<{ defaultOrder: SortOrder; name: string; value: string }>>
> = {
[ServerType.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,
},
],
[ServerType.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,
},
],
[ServerType.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,
},
],
};
const GENRE_LIST_FILTERS: Partial<
Record<ServerType, Array<{ defaultOrder: SortOrder; name: string; value: string }>>
> = {
@@ -474,6 +561,7 @@ const GENRE_LIST_FILTERS: Partial<
const FILTERS: Partial<Record<LibraryItem, any>> = {
[LibraryItem.ALBUM]: ALBUM_LIST_FILTERS,
[LibraryItem.ALBUM_ARTIST]: ALBUM_ARTIST_LIST_FILTERS,
[LibraryItem.ARTIST]: ARTIST_LIST_FILTERS,
[LibraryItem.GENRE]: GENRE_LIST_FILTERS,
[LibraryItem.SONG]: SONG_LIST_FILTERS,
};
@@ -0,0 +1,47 @@
import { parseAsString, useQueryState } from 'nuqs';
import { useCurrentServer } from '/@/renderer/store';
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
import { ItemListKey } from '/@/shared/types/types';
export const useSelectFilter = (
filterKey: string,
defaultValue: null | string,
listKey: ItemListKey,
) => {
const server = useCurrentServer();
const [persisted, setPersisted] = useLocalStorage({
defaultValue: defaultValue || '',
key: getPersistenceKey(server.id, listKey, filterKey),
});
const [value, setValue] = useQueryState(filterKey, getDefaultValue(defaultValue, persisted));
const handleSetValue = (newValue: string) => {
setValue(newValue);
setPersisted(newValue);
};
return {
[filterKey]: value ?? undefined,
setValue: handleSetValue,
value: value ?? undefined,
};
};
const getDefaultValue = (defaultValue: null | string, persisted: null | string) => {
if (persisted) {
return parseAsString.withDefault(persisted);
}
if (defaultValue) {
return parseAsString.withDefault(defaultValue);
}
return parseAsString;
};
const getPersistenceKey = (serverId: string, listKey: ItemListKey, filterKey: string) => {
return `${serverId}-list-${listKey}-${filterKey}`;
};
+6
View File
@@ -36,12 +36,17 @@ enum AlbumFilterKeys {
RECENTLY_PLAYED = 'recentlyPlayed',
}
enum ArtistFilterKeys {
ROLE = 'role',
}
enum SharedFilterKeys {
MUSIC_FOLDER_ID = 'musicFolderId',
SEARCH_TERM = 'searchTerm',
SORT_BY = 'sortBy',
SORT_ORDER = 'sortOrder',
}
enum SongFilterKeys {
_CUSTOM = '_custom',
ALBUM_IDS = 'albumIds',
@@ -59,6 +64,7 @@ const PaginationFilterKeys = {
export const FILTER_KEYS = {
ALBUM: AlbumFilterKeys,
ARTIST: ArtistFilterKeys,
PAGINATION: PaginationFilterKeys,
SHARED: SharedFilterKeys,
SONG: SongFilterKeys,