From 72f20ddd114f8c3c44c076a6e0f02ccbe29b90f8 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 13 Nov 2025 21:27:11 -0800 Subject: [PATCH] add new album artist list --- .../components/album-artist-list-content.tsx | 150 ++++-- .../album-artist-list-header-filters.tsx | 451 +----------------- .../components/album-artist-list-header.tsx | 70 +-- .../album-artist-list-infinite-grid.tsx | 68 +++ .../album-artist-list-infinite-table.tsx | 87 ++++ .../album-artist-list-paginated-grid.tsx | 84 ++++ .../album-artist-list-paginated-table.tsx | 98 ++++ .../hooks/use-album-artist-list-filters.ts | 29 ++ .../routes/album-artist-list-route.tsx | 46 +- .../components/list-sort-by-dropdown.tsx | 88 ++++ 10 files changed, 621 insertions(+), 550 deletions(-) create mode 100644 src/renderer/features/artists/components/album-artist-list-infinite-grid.tsx create mode 100644 src/renderer/features/artists/components/album-artist-list-infinite-table.tsx create mode 100644 src/renderer/features/artists/components/album-artist-list-paginated-grid.tsx create mode 100644 src/renderer/features/artists/components/album-artist-list-paginated-table.tsx create mode 100644 src/renderer/features/artists/hooks/use-album-artist-list-filters.ts diff --git a/src/renderer/features/artists/components/album-artist-list-content.tsx b/src/renderer/features/artists/components/album-artist-list-content.tsx index e7a9c5d32..96139df05 100644 --- a/src/renderer/features/artists/components/album-artist-list-content.tsx +++ b/src/renderer/features/artists/components/album-artist-list-content.tsx @@ -1,51 +1,139 @@ -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 { useAlbumArtistListFilters } from '/@/renderer/features/artists/hooks/use-album-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 AlbumArtistListGridView = lazy(() => - import('/@/renderer/features/artists/components/album-artist-list-grid-view').then( +const AlbumArtistListInfiniteGrid = lazy(() => + import('/@/renderer/features/artists/components/album-artist-list-infinite-grid').then( (module) => ({ - default: module.AlbumArtistListGridView, + default: module.AlbumArtistListInfiniteGrid, }), ), ); -const AlbumArtistListTableView = lazy(() => - import('/@/renderer/features/artists/components/album-artist-list-table-view').then( +const AlbumArtistListPaginatedGrid = lazy(() => + import('/@/renderer/features/artists/components/album-artist-list-paginated-grid').then( (module) => ({ - default: module.AlbumArtistListTableView, + default: module.AlbumArtistListPaginatedGrid, }), ), ); -interface AlbumArtistListContentProps { - gridRef: MutableRefObject; - itemCount?: number; - tableRef: MutableRefObject; -} +const AlbumArtistListInfiniteTable = lazy(() => + import('/@/renderer/features/artists/components/album-artist-list-infinite-table').then( + (module) => ({ + default: module.AlbumArtistListInfiniteTable, + }), + ), +); -export const AlbumArtistListContent = ({ - gridRef, - itemCount, - tableRef, -}: AlbumArtistListContentProps) => { - const { pageKey } = useListContext(); - const { display } = useListStoreByKey({ key: pageKey }); - const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.GRID; +const AlbumArtistListPaginatedTable = lazy(() => + import('/@/renderer/features/artists/components/album-artist-list-paginated-table').then( + (module) => ({ + default: module.AlbumArtistListPaginatedTable, + }), + ), +); + +export const AlbumArtistListContent = () => { + const { display, grid, itemsPerPage, pagination, table } = useListSettings( + ItemListKey.ALBUM_ARTIST, + ); return ( }> - {isGrid ? ( - - ) : ( - - )} + ); }; + +export const AlbumArtistListView = ({ + display, + grid, + itemsPerPage, + pagination, + table, +}: ItemListSettings) => { + const server = useCurrentServer(); + + const { query } = useAlbumArtistListFilters(); + + 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/album-artist-list-header-filters.tsx b/src/renderer/features/artists/components/album-artist-list-header-filters.tsx index 42c8bea1b..84a573bb0 100644 --- a/src/renderer/features/artists/components/album-artist-list-header-filters.tsx +++ b/src/renderer/features/artists/components/album-artist-list-header-filters.tsx @@ -1,451 +1,38 @@ -import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; - -import { IDatasource } from '@ag-grid-community/core'; -import { useQuery, useQueryClient } from '@tanstack/react-query'; -import debounce from 'lodash/debounce'; -import { MouseEvent, MutableRefObject, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; - -import i18n from '/@/i18n/i18n'; -import { 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 { sharedQueries } from '/@/renderer/features/shared/api/shared-api'; -import { FolderButton } from '/@/renderer/features/shared/components/folder-button'; 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 { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown'; +import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button'; import { useContainerQuery } from '/@/renderer/hooks'; -import { - AlbumArtistListFilter, - PersistedTableColumn, - useCurrentServer, - useListStoreActions, - useListStoreByKey, -} from '/@/renderer/store'; -import { Button } from '/@/shared/components/button/button'; 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 { - AlbumArtistListQuery, - AlbumArtistListSort, - LibraryItem, - ServerType, - SortOrder, -} from '/@/shared/types/domain-types'; -import { ListDisplayType } from '/@/shared/types/types'; +import { AlbumArtistListSort, 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: AlbumArtistListSort.ALBUM, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.duration', { postProcess: 'titleCase' }), - value: AlbumArtistListSort.DURATION, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.name', { postProcess: 'titleCase' }), - value: AlbumArtistListSort.NAME, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.random', { postProcess: 'titleCase' }), - value: AlbumArtistListSort.RANDOM, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }), - value: AlbumArtistListSort.RECENTLY_ADDED, - }, - // { defaultOrder: SortOrder.DESC, name: 'Release Date', value: AlbumArtistListSort.RELEASE_DATE }, - ], - navidrome: [ - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }), - value: AlbumArtistListSort.ALBUM_COUNT, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }), - value: AlbumArtistListSort.FAVORITED, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }), - value: AlbumArtistListSort.PLAY_COUNT, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.name', { postProcess: 'titleCase' }), - value: AlbumArtistListSort.NAME, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.rating', { postProcess: 'titleCase' }), - value: AlbumArtistListSort.RATING, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.songCount', { postProcess: 'titleCase' }), - value: AlbumArtistListSort.SONG_COUNT, - }, - ], - subsonic: [ - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }), - value: AlbumArtistListSort.ALBUM_COUNT, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }), - value: AlbumArtistListSort.FAVORITED, - }, - { - defaultOrder: SortOrder.ASC, - name: i18n.t('filter.name', { postProcess: 'titleCase' }), - value: AlbumArtistListSort.NAME, - }, - { - defaultOrder: SortOrder.DESC, - name: i18n.t('filter.rating', { postProcess: 'titleCase' }), - value: AlbumArtistListSort.RATING, - }, - ], -}; - -interface AlbumArtistListHeaderFiltersProps { - gridRef: MutableRefObject; - tableRef: MutableRefObject; -} - -export const AlbumArtistListHeaderFilters = ({ - gridRef, - tableRef, -}: AlbumArtistListHeaderFiltersProps) => { - 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 AlbumArtistListHeaderFilters = () => { const cq = useContainerQuery(); - 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: AlbumArtistListFilter) => { - const queryKey = queryKeys.albumArtists.list(server?.id || '', { - limit, - startIndex, - ...filters, - }); - - const albums = await queryClient.fetchQuery({ - gcTime: 1000 * 60 * 1, - queryFn: async ({ signal }) => - api.controller.getAlbumArtistList({ - apiClientProps: { - serverId: server?.id || '', - signal, - }, - query: { - limit, - startIndex, - ...filters, - }, - }), - queryKey, - }); - - return albums; - }, - [queryClient, server], - ); - - const handleFilterChange = useCallback( - async (filters: AlbumArtistListFilter) => { - 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.albumArtists.list(server?.id || '', { - limit, - startIndex, - ...filters, - }); - - const albumArtistsRes = await queryClient.fetchQuery({ - gcTime: 1000 * 60 * 1, - queryFn: async ({ signal }) => - api.controller.getAlbumArtistList({ - apiClientProps: { - serverId: server?.id || '', - signal, - }, - query: { - limit, - startIndex, - ...filters, - }, - }), - queryKey, - }); - - params.successCallback( - albumArtistsRes?.items || [], - albumArtistsRes?.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 AlbumArtistListSort, - sortOrder: sortOrder || SortOrder.ASC, - }, - itemType: LibraryItem.ALBUM_ARTIST, - key: pageKey, - }) as AlbumArtistListFilter; - - handleFilterChange(updatedFilters); - }, - [handleFilterChange, pageKey, server?.type, setFilter], - ); - - const handleSetMusicFolder = useCallback( - (e: MouseEvent) => { - if (!e.currentTarget?.value) return; - - let updatedFilters: AlbumArtistListFilter | null = null; - if (e.currentTarget.value === String(filter.musicFolderId)) { - updatedFilters = setFilter({ - data: { musicFolderId: undefined }, - itemType: LibraryItem.ALBUM_ARTIST, - key: pageKey, - }) as AlbumArtistListFilter; - } else { - updatedFilters = setFilter({ - data: { musicFolderId: e.currentTarget.value }, - itemType: LibraryItem.ALBUM_ARTIST, - key: pageKey, - }) as AlbumArtistListFilter; - } - - 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.ALBUM_ARTIST, - key: pageKey, - }) as AlbumArtistListFilter; - 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.albumArtists.list(server?.id || '') }); - handleFilterChange(filter); - }, [filter, handleFilterChange, queryClient, server?.id]); - - const isFolderFilterApplied = useMemo(() => { - return filter.musicFolderId !== undefined; - }, [filter.musicFolderId]); - return ( - - - - - - {FILTERS[server?.type as keyof typeof FILTERS].map((f) => ( - - {f.name} - - ))} - - + - - {server?.type === ServerType.JELLYFIN && ( - <> - - - - - - {musicFoldersQuery.data?.items.map((folder) => ( - - {folder.name} - - ))} - - - - )} - - - - - - - } - onClick={handleRefresh} - > - {t('common.refresh', { - postProcess: 'titleCase', - })} - - - + + + column.column)} + listKey={ItemListKey.ALBUM_ARTIST} tableColumnsData={ALBUMARTIST_TABLE_COLUMNS} /> diff --git a/src/renderer/features/artists/components/album-artist-list-header.tsx b/src/renderer/features/artists/components/album-artist-list-header.tsx index 8acd3b06d..5fda224c4 100644 --- a/src/renderer/features/artists/components/album-artist-list-header.tsx +++ b/src/renderer/features/artists/components/album-artist-list-header.tsx @@ -1,72 +1,40 @@ -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 { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-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 { AlbumArtistListFilter, 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 { AlbumArtistListQuery, LibraryItem } from '/@/shared/types/domain-types'; interface AlbumArtistListHeaderProps { - gridRef: MutableRefObject; - itemCount?: number; - tableRef: MutableRefObject; + title?: string; } -export const AlbumArtistListHeader = ({ - gridRef, - itemCount, - tableRef, -}: AlbumArtistListHeaderProps) => { +export const AlbumArtistListHeader = ({ title }: AlbumArtistListHeaderProps) => { const { t } = useTranslation(); - const server = useCurrentServer(); - const cq = useContainerQuery(); - const { filter, refresh, search } = useDisplayRefresh({ - gridRef, - itemCount, - itemType: LibraryItem.ALBUM_ARTIST, - server, - tableRef, - }); - - const handleSearch = debounce((e: ChangeEvent) => { - const updatedFilters = search(e) as AlbumArtistListFilter; - refresh(updatedFilters); - }, 500); + const { itemCount } = useListContext(); + const pageTitle = title || t('page.albumArtistList.title', { postProcess: 'titleCase' }); return ( - - - - - - {t('page.albumArtistList.title', { postProcess: 'titleCase' })} - - - {itemCount} - - - - - - + + + + + {pageTitle} + + {itemCount} + + + + + - + ); diff --git a/src/renderer/features/artists/components/album-artist-list-infinite-grid.tsx b/src/renderer/features/artists/components/album-artist-list-infinite-grid.tsx new file mode 100644 index 000000000..db0dc4fae --- /dev/null +++ b/src/renderer/features/artists/components/album-artist-list-infinite-grid.tsx @@ -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 { + AlbumArtistListQuery, + AlbumArtistListSort, + LibraryItem, + SortOrder, +} from '/@/shared/types/domain-types'; +import { ItemListKey } from '/@/shared/types/types'; + +interface AlbumArtistListInfiniteGridProps extends ItemListGridComponentProps {} + +export const AlbumArtistListInfiniteGrid = forwardRef( + ( + { + gap = 'md', + itemsPerPage = 100, + itemsPerRow, + query = { + sortBy: AlbumArtistListSort.NAME, + sortOrder: SortOrder.ASC, + }, + saveScrollOffset = true, + serverId, + }, + ref, + ) => { + const listCountQuery = artistsQueries.albumArtistListCount({ + query: { ...query }, + serverId: serverId, + }) as UseSuspenseQueryOptions; + + const listQueryFn = api.controller.getAlbumArtistList; + + const { data, onRangeChanged } = useItemListInfiniteLoader({ + eventKey: ItemListKey.ALBUM_ARTIST, + itemsPerPage, + itemType: LibraryItem.ALBUM_ARTIST, + listCountQuery, + listQueryFn, + query, + serverId, + }); + + const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({ + enabled: saveScrollOffset, + }); + + return ( + + ); + }, +); 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 new file mode 100644 index 000000000..ef1f3c033 --- /dev/null +++ b/src/renderer/features/artists/components/album-artist-list-infinite-table.tsx @@ -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 { + AlbumArtistListQuery, + AlbumArtistListSort, + LibraryItem, + SortOrder, +} from '/@/shared/types/domain-types'; +import { ItemListKey } from '/@/shared/types/types'; + +interface AlbumArtistListInfiniteTableProps + extends ItemListTableComponentProps {} + +export const AlbumArtistListInfiniteTable = forwardRef( + ( + { + autoFitColumns = false, + columns, + enableAlternateRowColors = false, + enableHorizontalBorders = false, + enableRowHoverHighlight = true, + enableSelection = true, + enableVerticalBorders = false, + itemsPerPage = 100, + query = { + sortBy: AlbumArtistListSort.NAME, + sortOrder: SortOrder.ASC, + }, + saveScrollOffset = true, + serverId, + size = 'default', + }, + ref, + ) => { + const listCountQuery = artistsQueries.albumArtistListCount({ + query: { ...query }, + serverId: serverId, + }) as UseSuspenseQueryOptions; + + const listQueryFn = api.controller.getAlbumArtistList; + + const { data, onRangeChanged } = useItemListInfiniteLoader({ + eventKey: ItemListKey.ALBUM_ARTIST, + itemsPerPage, + itemType: LibraryItem.ALBUM_ARTIST, + listCountQuery, + listQueryFn, + query, + serverId, + }); + + const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({ + enabled: saveScrollOffset, + }); + + return ( + + ); + }, +); diff --git a/src/renderer/features/artists/components/album-artist-list-paginated-grid.tsx b/src/renderer/features/artists/components/album-artist-list-paginated-grid.tsx new file mode 100644 index 000000000..504c2630c --- /dev/null +++ b/src/renderer/features/artists/components/album-artist-list-paginated-grid.tsx @@ -0,0 +1,84 @@ +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 { + AlbumArtistListQuery, + AlbumArtistListSort, + LibraryItem, + SortOrder, +} from '/@/shared/types/domain-types'; + +interface AlbumArtistListPaginatedGridProps + extends ItemListGridComponentProps {} + +export const AlbumArtistListPaginatedGrid = forwardRef( + ( + { + gap = 'md', + itemsPerPage = 100, + itemsPerRow, + query = { + sortBy: AlbumArtistListSort.NAME, + sortOrder: SortOrder.ASC, + }, + saveScrollOffset = true, + serverId, + }, + ref, + ) => { + const listCountQuery = artistsQueries.albumArtistListCount({ + query: { ...query }, + serverId: serverId, + }) as UseSuspenseQueryOptions; + + const listQueryFn = api.controller.getAlbumArtistList; + + const { currentPage, onChange } = useItemListPagination(); + + const { data, pageCount, totalItemCount } = useItemListPaginatedLoader({ + currentPage, + itemsPerPage, + itemType: LibraryItem.ALBUM_ARTIST, + listCountQuery, + listQueryFn, + query, + serverId, + }); + + const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({ + enabled: saveScrollOffset, + }); + + return ( + + + + ); + }, +); diff --git a/src/renderer/features/artists/components/album-artist-list-paginated-table.tsx b/src/renderer/features/artists/components/album-artist-list-paginated-table.tsx new file mode 100644 index 000000000..eddd7ce38 --- /dev/null +++ b/src/renderer/features/artists/components/album-artist-list-paginated-table.tsx @@ -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 { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination'; +import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination'; +import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list'; +import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; +import { ItemListTableComponentProps } from '/@/renderer/components/item-list/types'; +import { artistsQueries } from '/@/renderer/features/artists/api/artists-api'; +import { + AlbumArtistListQuery, + AlbumArtistListSort, + LibraryItem, + SortOrder, +} from '/@/shared/types/domain-types'; + +interface AlbumArtistListPaginatedTableProps + extends ItemListTableComponentProps {} + +export const AlbumArtistListPaginatedTable = forwardRef( + ( + { + autoFitColumns = false, + columns, + enableAlternateRowColors = false, + enableHorizontalBorders = false, + enableRowHoverHighlight = true, + enableSelection = true, + enableVerticalBorders = false, + itemsPerPage = 100, + query = { + sortBy: AlbumArtistListSort.NAME, + sortOrder: SortOrder.ASC, + }, + saveScrollOffset = true, + serverId, + size = 'default', + }, + ref, + ) => { + const listCountQuery = artistsQueries.albumArtistListCount({ + query: { ...query }, + serverId: serverId, + }) as UseSuspenseQueryOptions; + + const listQueryFn = api.controller.getAlbumArtistList; + + const { currentPage, onChange } = useItemListPagination(); + + const { data, pageCount, totalItemCount } = useItemListPaginatedLoader({ + currentPage, + itemsPerPage, + itemType: LibraryItem.ALBUM_ARTIST, + listCountQuery, + listQueryFn, + query, + serverId, + }); + + const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({ + enabled: saveScrollOffset, + }); + + return ( + + + + ); + }, +); diff --git a/src/renderer/features/artists/hooks/use-album-artist-list-filters.ts b/src/renderer/features/artists/hooks/use-album-artist-list-filters.ts new file mode 100644 index 000000000..f55debad0 --- /dev/null +++ b/src/renderer/features/artists/hooks/use-album-artist-list-filters.ts @@ -0,0 +1,29 @@ +import { useMusicFolderIdFilter } from '/@/renderer/features/shared/hooks/use-music-folder-id-filter'; +import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter'; +import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter'; +import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter'; +import { FILTER_KEYS } from '/@/renderer/features/shared/utils'; +import { AlbumArtistListSort } from '/@/shared/types/domain-types'; +import { ItemListKey } from '/@/shared/types/types'; + +export const useAlbumArtistListFilters = () => { + const { sortBy } = useSortByFilter(null, ItemListKey.ALBUM_ARTIST); + + const { sortOrder } = useSortOrderFilter(null, ItemListKey.ALBUM_ARTIST); + + const { musicFolderId } = useMusicFolderIdFilter(null, ItemListKey.ALBUM_ARTIST); + + const { searchTerm, setSearchTerm } = useSearchTermFilter(''); + + const query = { + [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, + }; +}; diff --git a/src/renderer/features/artists/routes/album-artist-list-route.tsx b/src/renderer/features/artists/routes/album-artist-list-route.tsx index 235c598c3..92726a9f0 100644 --- a/src/renderer/features/artists/routes/album-artist-list-route.tsx +++ b/src/renderer/features/artists/routes/album-artist-list-route.tsx @@ -1,56 +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 { AlbumArtistListContent } from '/@/renderer/features/artists/components/album-artist-list-content'; import { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-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 { AlbumArtistListQuery, LibraryItem } from '/@/shared/types/domain-types'; +import { ItemListKey } from '/@/shared/types/types'; const AlbumArtistListRoute = () => { - const gridRef = useRef(null); - const tableRef = useRef(null); - const pageKey = LibraryItem.ALBUM_ARTIST; - const server = useCurrentServer(); + const pageKey = ItemListKey.ALBUM_ARTIST; - const albumArtistListFilter = useListFilterByKey({ key: pageKey }); - - const itemCountCheck = useQuery( - artistsQueries.albumArtistListCount({ - options: { - gcTime: 1000 * 60, - }, - query: albumArtistListFilter, - serverId: server?.id, - }), - ); - - const itemCount = itemCountCheck.data === null ? undefined : itemCountCheck.data; + const [itemCount, setItemCount] = useState(undefined); const providerValue = useMemo(() => { return { id: undefined, + itemCount, pageKey, + setItemCount, }; - }, [pageKey]); + }, [itemCount, pageKey, setItemCount]); return ( - - {/* */} + + ); 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 03a4ae489..0571d5598 100644 --- a/src/renderer/features/shared/components/list-sort-by-dropdown.tsx +++ b/src/renderer/features/shared/components/list-sort-by-dropdown.tsx @@ -4,6 +4,7 @@ import { useCurrentServer } from '/@/renderer/store'; import { Button } from '/@/shared/components/button/button'; import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu'; import { + AlbumArtistListSort, AlbumListSort, LibraryItem, ServerType, @@ -357,7 +358,94 @@ const SONG_LIST_FILTERS: Partial< ], }; +const ALBUM_ARTIST_LIST_FILTERS: Partial< + Record> +> = { + [ServerType.JELLYFIN]: [ + { + defaultOrder: SortOrder.ASC, + name: i18n.t('filter.album', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.ALBUM, + }, + { + defaultOrder: SortOrder.DESC, + name: i18n.t('filter.duration', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.DURATION, + }, + { + defaultOrder: SortOrder.ASC, + name: i18n.t('filter.name', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.NAME, + }, + { + defaultOrder: SortOrder.ASC, + name: i18n.t('filter.random', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.RANDOM, + }, + { + defaultOrder: SortOrder.DESC, + name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.RECENTLY_ADDED, + }, + ], + [ServerType.NAVIDROME]: [ + { + defaultOrder: SortOrder.DESC, + name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.ALBUM_COUNT, + }, + { + defaultOrder: SortOrder.DESC, + name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.FAVORITED, + }, + { + defaultOrder: SortOrder.DESC, + name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.PLAY_COUNT, + }, + { + defaultOrder: SortOrder.ASC, + name: i18n.t('filter.name', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.NAME, + }, + { + defaultOrder: SortOrder.DESC, + name: i18n.t('filter.rating', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.RATING, + }, + { + defaultOrder: SortOrder.DESC, + name: i18n.t('filter.songCount', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.SONG_COUNT, + }, + ], + [ServerType.SUBSONIC]: [ + { + defaultOrder: SortOrder.DESC, + name: i18n.t('filter.albumCount', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.ALBUM_COUNT, + }, + { + defaultOrder: SortOrder.DESC, + name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.FAVORITED, + }, + { + defaultOrder: SortOrder.ASC, + name: i18n.t('filter.name', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.NAME, + }, + { + defaultOrder: SortOrder.DESC, + name: i18n.t('filter.rating', { postProcess: 'titleCase' }), + value: AlbumArtistListSort.RATING, + }, + ], +}; + const FILTERS: Partial> = { [LibraryItem.ALBUM]: ALBUM_LIST_FILTERS, + [LibraryItem.ALBUM_ARTIST]: ALBUM_ARTIST_LIST_FILTERS, [LibraryItem.SONG]: SONG_LIST_FILTERS, };