diff --git a/src/renderer/api/query-keys.ts b/src/renderer/api/query-keys.ts index d779d9059..83077ccd1 100644 --- a/src/renderer/api/query-keys.ts +++ b/src/renderer/api/query-keys.ts @@ -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) { diff --git a/src/renderer/components/virtual-table/table-config-dropdown.tsx b/src/renderer/components/virtual-table/table-config-dropdown.tsx index cce9a1a65..5b1a84d05 100644 --- a/src/renderer/components/virtual-table/table-config-dropdown.tsx +++ b/src/renderer/components/virtual-table/table-config-dropdown.tsx @@ -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' }), diff --git a/src/renderer/features/artists/api/artists-api.ts b/src/renderer/features/artists/api/artists-api.ts index cc8f07ca1..cb01ea6fa 100644 --- a/src/renderer/features/artists/api/artists-api.ts +++ b/src/renderer/features/artists/api/artists-api.ts @@ -51,15 +51,29 @@ export const artistsQueries = { ...args.options, }); }, - artistListCount: (args: QueryHookArgs>) => { + artistList: (args: QueryHookArgs) => { 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>) => { + 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, ), diff --git a/src/renderer/features/artists/components/album-artist-list-infinite-table.tsx b/src/renderer/features/artists/components/album-artist-list-infinite-table.tsx index ef1f3c033..152639bd2 100644 --- a/src/renderer/features/artists/components/album-artist-list-infinite-table.tsx +++ b/src/renderer/features/artists/components/album-artist-list-infinite-table.tsx @@ -68,6 +68,7 @@ export const AlbumArtistListInfiniteTable = forwardRef - 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; - itemCount?: number; - tableRef: MutableRefObject; -} +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 ( }> - {isGrid ? ( - - ) : ( - - )} + ); }; + +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 ( + + ); + } + case ListPaginationType.PAGINATED: { + return ( + + ); + } + default: + return null; + } + } + case ListDisplayType.TABLE: { + switch (pagination) { + case ListPaginationType.INFINITE: { + return ( + + ); + } + case ListPaginationType.PAGINATED: { + return ( + + ); + } + default: + return null; + } + } + } + + return null; +}; diff --git a/src/renderer/features/artists/components/artist-list-header-filters.tsx b/src/renderer/features/artists/components/artist-list-header-filters.tsx index 094f5936b..89b3b6dbd 100644 --- a/src/renderer/features/artists/components/artist-list-header-filters.tsx +++ b/src/renderer/features/artists/components/artist-list-header-filters.tsx @@ -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; - tableRef: MutableRefObject; -} - -export const ArtistListHeaderFilters = ({ gridRef, tableRef }: ArtistListHeaderFiltersProps) => { - const { t } = useTranslation(); - const queryClient = useQueryClient(); - const server = useCurrentServer(); - const { pageKey } = useListContext(); - const { display, filter, grid, table } = useListStoreByKey({ - 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) => { - 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) => { - 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 ( - - - - - - {FILTERS[server?.type as keyof typeof FILTERS].map((f) => ( - - {f.name} - - ))} - - + - - {server?.type === ServerType.JELLYFIN && ( + + + {rolesQuery.data && rolesQuery.data.length > 0 && ( <> - - - - - - {musicFoldersQuery.data?.items.map((folder) => ( - - {folder.name} - - ))} - - + )} - {roles.data?.length && ( - <> - handleSetValue(value ?? '')} + value={value ?? ''} + /> + ); + + return ( + + + + + + {selectData.map((option) => { + const optionValue = getOptionValue(option); + const optionLabel = getOptionLabel(option); + + return ( + handleSetValue(optionValue)} + value={optionValue} + > + {optionLabel} + + ); + })} + + + ); +}; diff --git a/src/renderer/features/shared/components/list-sort-by-dropdown.tsx b/src/renderer/features/shared/components/list-sort-by-dropdown.tsx index 34c596ec2..820447ad4 100644 --- a/src/renderer/features/shared/components/list-sort-by-dropdown.tsx +++ b/src/renderer/features/shared/components/list-sort-by-dropdown.tsx @@ -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.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> > = { @@ -474,6 +561,7 @@ const GENRE_LIST_FILTERS: Partial< const FILTERS: Partial> = { [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, }; diff --git a/src/renderer/features/shared/hooks/use-select-filter.ts b/src/renderer/features/shared/hooks/use-select-filter.ts new file mode 100644 index 000000000..d85a621b2 --- /dev/null +++ b/src/renderer/features/shared/hooks/use-select-filter.ts @@ -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}`; +}; diff --git a/src/renderer/features/shared/utils.ts b/src/renderer/features/shared/utils.ts index 9951f3212..c61e80938 100644 --- a/src/renderer/features/shared/utils.ts +++ b/src/renderer/features/shared/utils.ts @@ -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,