diff --git a/src/renderer/features/albums/components/album-list-content.tsx b/src/renderer/features/albums/components/album-list-content.tsx index 33d0a893..c82d1891 100644 --- a/src/renderer/features/albums/components/album-list-content.tsx +++ b/src/renderer/features/albums/components/album-list-content.tsx @@ -2,8 +2,8 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li import { lazy, MutableRefObject, Suspense } from 'react'; import { Spinner } from '/@/renderer/components'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; -import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; -import { useAlbumListStore } from '/@/renderer/store'; +import { useListContext } from '/@/renderer/context/list-context'; +import { useListStoreByKey } from '/@/renderer/store'; import { ListDisplayType } from '/@/renderer/types'; const AlbumListGridView = lazy(() => @@ -25,8 +25,8 @@ interface AlbumListContentProps { } export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListContentProps) => { - const { id, pageKey } = useAlbumListContext(); - const { display } = useAlbumListStore({ id, key: pageKey }); + const { pageKey } = useListContext(); + const { display } = useListStoreByKey({ key: pageKey }); return ( }> diff --git a/src/renderer/features/albums/components/album-list-grid-view.tsx b/src/renderer/features/albums/components/album-list-grid-view.tsx index 6a574569..414369d7 100644 --- a/src/renderer/features/albums/components/album-list-grid-view.tsx +++ b/src/renderer/features/albums/components/album-list-grid-view.tsx @@ -1,35 +1,35 @@ +import { QueryKey, useQueryClient } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; -import { useQueryClient } from '@tanstack/react-query'; import AutoSizer from 'react-virtualized-auto-sizer'; import { ListOnScrollProps } from 'react-window'; import { controller } from '/@/renderer/api/controller'; -import { queryKeys } from '/@/renderer/api/query-keys'; -import { Album, AlbumListQuery, AlbumListSort, LibraryItem } from '/@/renderer/api/types'; +import { queryKeys, splitPaginatedQuery } from '/@/renderer/api/query-keys'; +import { + Album, + AlbumListQuery, + AlbumListResponse, + AlbumListSort, + LibraryItem, +} from '/@/renderer/api/types'; import { ALBUM_CARD_ROWS } from '/@/renderer/components'; import { VirtualGridAutoSizerContainer, VirtualInfiniteGrid, } from '/@/renderer/components/virtual-grid'; -import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; +import { useListContext } from '/@/renderer/context/list-context'; import { usePlayQueueAdd } from '/@/renderer/features/player'; -import { AppRoute } from '/@/renderer/router/routes'; -import { - useAlbumListFilter, - useAlbumListStore, - useCurrentServer, - useListStoreActions, -} from '/@/renderer/store'; -import { CardRow, ListDisplayType } from '/@/renderer/types'; import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared'; +import { AppRoute } from '/@/renderer/router/routes'; +import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store'; +import { CardRow, ListDisplayType } from '/@/renderer/types'; export const AlbumListGridView = ({ gridRef, itemCount }: any) => { const queryClient = useQueryClient(); const server = useCurrentServer(); const handlePlayQueueAdd = usePlayQueueAdd(); - const { id, pageKey } = useAlbumListContext(); - const { grid, display } = useAlbumListStore({ id, key: pageKey }); + const { pageKey, customFilters } = useListContext(); + const { grid, display, filter } = useListStoreByKey({ key: pageKey }); const { setGrid } = useListStoreActions(); - const filter = useAlbumListFilter({ id, key: pageKey }); const createFavoriteMutation = useCreateFavorite({}); const deleteFavoriteMutation = useDeleteFavorite({}); @@ -129,27 +129,56 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => { [pageKey, setGrid], ); + const fetchInitialData = useCallback(() => { + const query: Omit = { + ...filter, + ...customFilters, + }; + + const queriesFromCache: [QueryKey, AlbumListResponse][] = queryClient.getQueriesData({ + exact: false, + fetchStatus: 'idle', + queryKey: queryKeys.albums.list(server?.id || '', query), + stale: false, + }); + + const itemData = []; + + for (const [, data] of queriesFromCache) { + const { items, startIndex } = data || {}; + + if (items && startIndex !== undefined) { + let itemIndex = 0; + for ( + let rowIndex = startIndex; + rowIndex < startIndex + items.length; + rowIndex += 1 + ) { + itemData[rowIndex] = items[itemIndex]; + itemIndex += 1; + } + } + } + + return itemData; + }, [customFilters, filter, queryClient, server?.id]); + const fetch = useCallback( async ({ skip, take }: { skip: number; take: number }) => { if (!server) { return []; } - const query: AlbumListQuery = { + const listQuery: AlbumListQuery = { limit: take, startIndex: skip, ...filter, - _custom: { - jellyfin: { - ...filter._custom?.jellyfin, - }, - navidrome: { - ...filter._custom?.navidrome, - }, - }, + ...customFilters, }; - const queryKey = queryKeys.albums.list(server?.id || '', query); + const { query, pagination } = splitPaginatedQuery(listQuery); + + const queryKey = queryKeys.albums.list(server?.id || '', query, pagination); const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) => controller.getAlbumList({ @@ -157,13 +186,13 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => { server, signal, }, - query, + query: listQuery, }), ); return albums; }, - [filter, queryClient, server], + [customFilters, filter, queryClient, server], ); return ( @@ -176,6 +205,7 @@ export const AlbumListGridView = ({ gridRef, itemCount }: any) => { cardRows={cardRows} display={display || ListDisplayType.CARD} fetchFn={fetch} + fetchInitialData={fetchInitialData} handleFavorite={handleFavorite} handlePlayQueueAdd={handlePlayQueueAdd} height={height} diff --git a/src/renderer/features/albums/components/album-list-header-filters.tsx b/src/renderer/features/albums/components/album-list-header-filters.tsx index be4f61be..651e62a9 100644 --- a/src/renderer/features/albums/components/album-list-header-filters.tsx +++ b/src/renderer/features/albums/components/album-list-header-filters.tsx @@ -1,38 +1,36 @@ -import { MutableRefObject, useCallback, MouseEvent, ChangeEvent, useMemo } from 'react'; -import { IDatasource } from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { Divider, Flex, Group, Stack } from '@mantine/core'; import { openModal } from '@mantine/modals'; import { useQueryClient } from '@tanstack/react-query'; +import { ChangeEvent, MouseEvent, MutableRefObject, useCallback, useMemo } from 'react'; import { + RiAddBoxFill, + RiAddCircleFill, + RiFilterFill, RiFolder2Line, RiMoreFill, - RiAddBoxFill, RiPlayFill, - RiAddCircleFill, RiRefreshLine, RiSettings3Fill, - RiFilterFill, } from 'react-icons/ri'; -import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; -import { AlbumListQuery, AlbumListSort, LibraryItem, SortOrder } from '/@/renderer/api/types'; +import { AlbumListSort, LibraryItem, SortOrder } from '/@/renderer/api/types'; import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components'; -import { useContainerQuery } from '/@/renderer/hooks'; -import { - AlbumListFilter, - useAlbumListStore, - useCurrentServer, - useListStoreActions, -} from '/@/renderer/store'; -import { ServerType, Play, ListDisplayType, TableColumn } from '/@/renderer/types'; -import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared'; -import { usePlayQueueAdd } from '/@/renderer/features/player'; -import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters'; -import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters'; -import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; +import { useListContext } from '/@/renderer/context/list-context'; +import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters'; +import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters'; +import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared'; +import { useContainerQuery } from '/@/renderer/hooks'; +import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh'; +import { + AlbumListFilter, + useCurrentServer, + useListStoreActions, + useListStoreByKey, +} from '/@/renderer/store'; +import { ListDisplayType, Play, ServerType, TableColumn } from '/@/renderer/types'; const FILTERS = { jellyfin: [ @@ -77,26 +75,23 @@ const FILTERS = { }; interface AlbumListHeaderFiltersProps { - customFilters?: Partial; gridRef: MutableRefObject; - itemCount?: number; tableRef: MutableRefObject; } -export const AlbumListHeaderFilters = ({ - customFilters, - gridRef, - tableRef, - itemCount, -}: AlbumListHeaderFiltersProps) => { +export const AlbumListHeaderFilters = ({ gridRef, tableRef }: AlbumListHeaderFiltersProps) => { const queryClient = useQueryClient(); - const { id, pageKey } = useAlbumListContext(); + const { pageKey, customFilters, handlePlay } = useListContext(); const server = useCurrentServer(); - const { setFilter, setTablePagination, setTable, setGrid, setDisplayType } = - useListStoreActions(); - const { display, filter, table, grid } = useAlbumListStore({ id, key: pageKey }); + const { setFilter, setTable, setGrid, setDisplayType } = useListStoreActions(); + const { display, filter, table, grid } = useListStoreByKey({ key: pageKey }); const cq = useContainerQuery(); + const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({ + itemType: LibraryItem.ALBUM, + server, + }); + const musicFoldersQuery = useMusicFolders({ query: null, serverId: server?.id }); const sortByLabel = @@ -107,123 +102,21 @@ export const AlbumListHeaderFilters = ({ const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER; - const fetch = useCallback( - async (skip: number, take: number, filters: AlbumListFilter) => { - const query: AlbumListQuery = { - limit: take, - startIndex: skip, - ...filters, - _custom: { - jellyfin: { - ...filters._custom?.jellyfin, - ...customFilters?._custom?.jellyfin, - }, - navidrome: { - ...filters._custom?.navidrome, - ...customFilters?._custom?.navidrome, - }, - }, - ...customFilters, - }; - - const queryKey = queryKeys.albums.list(server?.id || '', query); - - const albums = await queryClient.fetchQuery( - queryKey, - async ({ signal }) => - api.controller.getAlbumList({ - apiClientProps: { - server, - signal, - }, - query, - }), - { cacheTime: 1000 * 60 * 1 }, - ); - - return albums; - }, - [customFilters, queryClient, server], - ); - - const handleFilterChange = useCallback( - async (filters: AlbumListFilter) => { + const onFilterChange = useCallback( + (filter: AlbumListFilter) => { if (isGrid) { - 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); - } else { - const dataSource: IDatasource = { - getRows: async (params) => { - const limit = params.endRow - params.startRow; - const startIndex = params.startRow; - - const query: AlbumListQuery = { - limit, - startIndex, - ...filters, - ...customFilters, - _custom: { - jellyfin: { - ...filters._custom?.jellyfin, - ...customFilters?._custom?.jellyfin, - }, - navidrome: { - ...filters._custom?.navidrome, - ...customFilters?._custom?.navidrome, - }, - }, - }; - - const queryKey = queryKeys.albums.list(server?.id || '', query); - - const albumsRes = await queryClient.fetchQuery( - queryKey, - async ({ signal }) => - api.controller.getAlbumList({ - apiClientProps: { - server, - signal, - }, - query, - }), - { cacheTime: 1000 * 60 * 1 }, - ); - - return params.successCallback( - albumsRes?.items || [], - albumsRes?.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: 'album' }); - } + handleRefreshGrid(gridRef, { + ...filter, + ...customFilters, + }); } + + handleRefreshTable(tableRef, { + ...filter, + ...customFilters, + }); }, - [ - isGrid, - gridRef, - fetch, - tableRef, - display, - customFilters, - server, - queryClient, - setTablePagination, - ], + [customFilters, gridRef, handleRefreshGrid, handleRefreshTable, isGrid, tableRef], ); const handleOpenFiltersModal = () => { @@ -232,19 +125,19 @@ export const AlbumListHeaderFilters = ({ <> {server?.type === ServerType.NAVIDROME ? ( ) : ( )} @@ -255,8 +148,8 @@ export const AlbumListHeaderFilters = ({ const handleRefresh = useCallback(() => { queryClient.invalidateQueries(queryKeys.albums.list(server?.id || '')); - handleFilterChange(filter); - }, [filter, handleFilterChange, queryClient, server?.id]); + onFilterChange(filter); + }, [filter, onFilterChange, queryClient, server?.id]); const handleSetSortBy = useCallback( (e: MouseEvent) => { @@ -267,17 +160,18 @@ export const AlbumListHeaderFilters = ({ )?.defaultOrder; const updatedFilters = setFilter({ + customFilters, data: { sortBy: e.currentTarget.value as AlbumListSort, sortOrder: sortOrder || SortOrder.ASC, }, itemType: LibraryItem.ALBUM, - key: 'album', + key: pageKey, }) as AlbumListFilter; - handleFilterChange(updatedFilters); + onFilterChange(updatedFilters); }, - [handleFilterChange, server?.type, setFilter], + [customFilters, onFilterChange, pageKey, server?.type, setFilter], ); const handleSetMusicFolder = useCallback( @@ -287,86 +181,50 @@ export const AlbumListHeaderFilters = ({ let updatedFilters = null; if (e.currentTarget.value === String(filter.musicFolderId)) { updatedFilters = setFilter({ + customFilters, data: { musicFolderId: undefined }, itemType: LibraryItem.ALBUM, - key: 'album', + key: pageKey, }) as AlbumListFilter; } else { updatedFilters = setFilter({ + customFilters, data: { musicFolderId: e.currentTarget.value }, itemType: LibraryItem.ALBUM, - key: 'album', + key: pageKey, }) as AlbumListFilter; } - handleFilterChange(updatedFilters); + onFilterChange(updatedFilters); }, - [handleFilterChange, filter.musicFolderId, setFilter], + [filter.musicFolderId, onFilterChange, setFilter, customFilters, pageKey], ); const handleToggleSortOrder = useCallback(() => { const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; const updatedFilters = setFilter({ + customFilters, data: { sortOrder: newSortOrder }, itemType: LibraryItem.ALBUM, - key: 'album', + key: pageKey, }) as AlbumListFilter; - handleFilterChange(updatedFilters); - }, [filter.sortOrder, handleFilterChange, setFilter]); - - const handlePlayQueueAdd = usePlayQueueAdd(); - - const handlePlay = async (playType: Play) => { - if (!itemCount || itemCount === 0 || !server) return; - - const query = { - startIndex: 0, - ...filter, - ...customFilters, - _custom: { - jellyfin: { - ...filter._custom?.jellyfin, - ...customFilters?._custom?.jellyfin, - }, - navidrome: { - ...filter._custom?.navidrome, - ...customFilters?._custom?.navidrome, - }, - }, - }; - const queryKey = queryKeys.albums.list(server?.id || '', query); - - const albumListRes = await queryClient.fetchQuery({ - queryFn: ({ signal }) => - api.controller.getAlbumList({ apiClientProps: { server, signal }, query }), - queryKey, - }); - - const albumIds = albumListRes?.items?.map((a) => a.id) || []; - - handlePlayQueueAdd?.({ - byItemType: { - id: albumIds, - type: LibraryItem.ALBUM, - }, - playType, - }); - }; + onFilterChange(updatedFilters); + }, [customFilters, filter.sortOrder, onFilterChange, pageKey, setFilter]); const handleItemSize = (e: number) => { if (isGrid) { - setGrid({ data: { itemsPerRow: e }, key: 'album' }); + setGrid({ data: { itemsPerRow: e }, key: pageKey }); } else { - setTable({ data: { rowHeight: e }, key: 'album' }); + setTable({ data: { rowHeight: e }, key: pageKey }); } }; const handleSetViewType = useCallback( (e: MouseEvent) => { if (!e.currentTarget?.value) return; - setDisplayType({ data: e.currentTarget.value as ListDisplayType, key: 'album' }); + setDisplayType({ data: e.currentTarget.value as ListDisplayType, key: pageKey }); }, - [setDisplayType], + [pageKey, setDisplayType], ); const handleTableColumns = (values: TableColumn[]) => { @@ -375,7 +233,7 @@ export const AlbumListHeaderFilters = ({ if (values.length === 0) { return setTable({ data: { columns: [] }, - key: 'album', + key: pageKey, }); } @@ -383,20 +241,20 @@ export const AlbumListHeaderFilters = ({ if (values.length > existingColumns.length) { const newColumn = { column: values[values.length - 1], width: 100 }; - setTable({ data: { columns: [...existingColumns, newColumn] }, key: 'album' }); + 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: 'album' }); + setTable({ data: { columns: newColumns }, key: pageKey }); } return tableRef.current?.api.sizeColumnsToFit(); }; const handleAutoFitColumns = (e: ChangeEvent) => { - setTable({ data: { autoFit: e.currentTarget.checked }, key: 'album' }); + setTable({ data: { autoFit: e.currentTarget.checked }, key: pageKey }); if (e.currentTarget.checked) { tableRef.current?.api.sizeColumnsToFit(); @@ -511,19 +369,19 @@ export const AlbumListHeaderFilters = ({ } - onClick={() => handlePlay(Play.NOW)} + onClick={() => handlePlay?.({ playType: Play.NOW })} > Play } - onClick={() => handlePlay(Play.LAST)} + onClick={() => handlePlay?.({ playType: Play.LAST })} > Add to queue } - onClick={() => handlePlay(Play.NEXT)} + onClick={() => handlePlay?.({ playType: Play.NEXT })} > Add to queue next diff --git a/src/renderer/features/albums/components/album-list-header.tsx b/src/renderer/features/albums/components/album-list-header.tsx index 4119b74e..0555bccf 100644 --- a/src/renderer/features/albums/components/album-list-header.tsx +++ b/src/renderer/features/albums/components/album-list-header.tsx @@ -1,214 +1,60 @@ -import type { ChangeEvent, MutableRefObject } from 'react'; -import { useCallback } from 'react'; -import { IDatasource } from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { Flex, Group, Stack } from '@mantine/core'; -import { useQueryClient } from '@tanstack/react-query'; import debounce from 'lodash/debounce'; -import { api } from '/@/renderer/api'; -import { controller } from '/@/renderer/api/controller'; -import { queryKeys } from '/@/renderer/api/query-keys'; -import { AlbumListQuery, LibraryItem } from '/@/renderer/api/types'; +import type { ChangeEvent, MutableRefObject } from 'react'; +import { useListFilterRefresh } from '../../../hooks/use-list-filter-refresh'; +import { LibraryItem } from '/@/renderer/api/types'; import { PageHeader, SearchInput } from '/@/renderer/components'; +import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; +import { useListContext } from '/@/renderer/context/list-context'; +import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters'; import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared'; import { useContainerQuery } from '/@/renderer/hooks'; import { AlbumListFilter, - useAlbumListFilter, - useAlbumListStore, useCurrentServer, useListStoreActions, + useListStoreByKey, + usePlayButtonBehavior, } from '/@/renderer/store'; -import { ListDisplayType, Play } from '/@/renderer/types'; -import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters'; -import { usePlayQueueAdd } from '/@/renderer/features/player'; -import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; -import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; -import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; +import { ListDisplayType } from '/@/renderer/types'; interface AlbumListHeaderProps { - customFilters?: Partial; gridRef: MutableRefObject; itemCount?: number; tableRef: MutableRefObject; title?: string; } -export const AlbumListHeader = ({ - itemCount, - gridRef, - tableRef, - title, - customFilters, -}: AlbumListHeaderProps) => { - const queryClient = useQueryClient(); +export const AlbumListHeader = ({ itemCount, gridRef, tableRef, title }: AlbumListHeaderProps) => { const server = useCurrentServer(); const { setFilter, setTablePagination } = useListStoreActions(); const cq = useContainerQuery(); - const { id, pageKey } = useAlbumListContext(); - const { display } = useAlbumListStore({ id, key: pageKey }); - const filter = useAlbumListFilter({ id, key: pageKey }); + const { pageKey, handlePlay } = useListContext(); + const { display, filter } = useListStoreByKey({ key: pageKey }); + const playButtonBehavior = usePlayButtonBehavior(); - const fetch = useCallback( - async (skip: number, take: number, filters: AlbumListFilter) => { - const query: AlbumListQuery = { - limit: take, - startIndex: skip, - ...filters, - ...customFilters, - _custom: { - jellyfin: { - ...filters._custom?.jellyfin, - ...customFilters?._custom?.jellyfin, - }, - navidrome: { - ...filters._custom?.navidrome, - ...customFilters?._custom?.navidrome, - }, - }, - }; - - const queryKey = queryKeys.albums.list(server?.id || '', query); - - const albums = await queryClient.fetchQuery( - queryKey, - async ({ signal }) => - controller.getAlbumList({ - apiClientProps: { - server, - signal, - }, - query, - }), - { cacheTime: 1000 * 60 * 1 }, - ); - - return albums; - }, - [customFilters, queryClient, server], - ); - - const handleFilterChange = useCallback( - async (filters: AlbumListFilter) => { - 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 query: AlbumListQuery = { - limit, - startIndex, - ...filters, - ...customFilters, - _custom: { - jellyfin: { - ...filters._custom?.jellyfin, - ...customFilters?._custom?.jellyfin, - }, - navidrome: { - ...filters._custom?.navidrome, - ...customFilters?._custom?.navidrome, - }, - }, - }; - - const queryKey = queryKeys.albums.list(server?.id || '', query); - - const albumsRes = await queryClient.fetchQuery( - queryKey, - async ({ signal }) => - api.controller.getAlbumList({ - apiClientProps: { - server, - signal, - }, - query, - }), - { cacheTime: 1000 * 60 * 1 }, - ); - - params.successCallback( - albumsRes?.items || [], - albumsRes?.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: 'album' }); - } - } 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, customFilters, server, queryClient, setTablePagination, gridRef, fetch], - ); + const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({ + itemType: LibraryItem.ALBUM, + server, + }); const handleSearch = debounce((e: ChangeEvent) => { - const previousSearchTerm = filter.searchTerm; const searchTerm = e.target.value === '' ? undefined : e.target.value; const updatedFilters = setFilter({ data: { searchTerm }, itemType: LibraryItem.ALBUM, - key: 'album', + key: pageKey, }) as AlbumListFilter; - if (previousSearchTerm !== searchTerm) handleFilterChange(updatedFilters); + + if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) { + handleRefreshTable(tableRef, updatedFilters); + setTablePagination({ data: { currentPage: 0 }, key: pageKey }); + } else { + handleRefreshGrid(gridRef, updatedFilters); + } }, 500); - const handlePlayQueueAdd = usePlayQueueAdd(); - const playButtonBehavior = usePlayButtonBehavior(); - - const handlePlay = async (playType: Play) => { - if (!itemCount || itemCount === 0) return; - - const query = { - startIndex: 0, - ...filter, - ...customFilters, - _custom: { - jellyfin: { - ...filter._custom?.jellyfin, - ...customFilters?._custom?.jellyfin, - }, - navidrome: { - ...filter._custom?.navidrome, - ...customFilters?._custom?.navidrome, - }, - }, - }; - const queryKey = queryKeys.albums.list(server?.id || '', query); - - const albumListRes = await queryClient.fetchQuery({ - queryFn: ({ signal }) => - api.controller.getAlbumList({ apiClientProps: { server, signal }, query }), - queryKey, - }); - - const albumIds = albumListRes?.items?.map((item) => item.id) || []; - - handlePlayQueueAdd?.({ - byItemType: { - id: albumIds, - type: LibraryItem.ALBUM, - }, - playType, - }); - }; - return ( handlePlay(playButtonBehavior)} + onClick={() => handlePlay?.({ playType: playButtonBehavior })} /> {title || 'Albums'} diff --git a/src/renderer/features/albums/components/album-list-table-view.tsx b/src/renderer/features/albums/components/album-list-table-view.tsx index cf9d6129..ecbf3f8f 100644 --- a/src/renderer/features/albums/components/album-list-table-view.tsx +++ b/src/renderer/features/albums/components/album-list-table-view.tsx @@ -1,68 +1,22 @@ -import { useCallback } from 'react'; -import { api } from '/@/renderer/api'; -import { queryKeys } from '/@/renderer/api/query-keys'; -import { AlbumListQuery, AlbumListResponse, LibraryItem } from '/@/renderer/api/types'; -import { VirtualTable } from '/@/renderer/components/virtual-table'; -import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; -import { - useCurrentServer, - useAlbumListFilter, - useListStoreActions, - useAlbumListStore, -} from '/@/renderer/store'; -import { ALBUM_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; +import { useVirtualTable } from '../../../components/virtual-table/hooks/use-virtual-table'; +import { LibraryItem } from '/@/renderer/api/types'; import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid'; -import { - useVirtualTable, - AgGridFetchFn, -} from '../../../components/virtual-table/hooks/use-virtual-table'; +import { VirtualTable } from '/@/renderer/components/virtual-table'; +import { useListContext } from '/@/renderer/context/list-context'; +import { ALBUM_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; +import { useCurrentServer } from '/@/renderer/store'; export const AlbumListTableView = ({ tableRef, itemCount }: any) => { const server = useCurrentServer(); - const { id, pageKey } = useAlbumListContext(); - const filter = useAlbumListFilter({ id, key: pageKey }); - const { setTable, setTablePagination } = useListStoreActions(); - const listProperties = useAlbumListStore({ id, key: pageKey }); + const { pageKey, customFilters } = useListContext(); - const fetchFn: AgGridFetchFn< - AlbumListResponse, - Omit - > = useCallback( - async ({ filter, limit, startIndex }, signal) => { - const res = api.controller.getAlbumList({ - apiClientProps: { - server, - signal, - }, - query: { - ...filter, - limit, - sortBy: filter.sortBy, - sortOrder: filter.sortOrder, - startIndex, - }, - }); - - return res; - }, - [server], - ); - - const tableProps = useVirtualTable>({ + const tableProps = useVirtualTable({ contextMenu: ALBUM_CONTEXT_MENU_ITEMS, - fetch: { - filter, - fn: fetchFn, - itemCount, - queryKey: queryKeys.albums.list, - server, - }, + customFilters, itemCount, itemType: LibraryItem.ALBUM, pageKey, - properties: listProperties, - setTable, - setTablePagination, + server, tableRef, }); @@ -71,7 +25,7 @@ export const AlbumListTableView = ({ tableRef, itemCount }: any) => { diff --git a/src/renderer/features/albums/components/jellyfin-album-filters.tsx b/src/renderer/features/albums/components/jellyfin-album-filters.tsx index baa665c2..89330c11 100644 --- a/src/renderer/features/albums/components/jellyfin-album-filters.tsx +++ b/src/renderer/features/albums/components/jellyfin-album-filters.tsx @@ -1,28 +1,29 @@ -import { ChangeEvent, useMemo, useState } from 'react'; import { Divider, Group, Stack } from '@mantine/core'; -import { MultiSelect, NumberInput, SpinnerIcon, Switch, Text } from '/@/renderer/components'; -import { AlbumListFilter, useAlbumListFilter, useListStoreActions } from '/@/renderer/store'; import debounce from 'lodash/debounce'; -import { useGenreList } from '/@/renderer/features/genres'; +import { ChangeEvent, useMemo, useState } from 'react'; +import { useListFilterByKey } from '../../../store/list.store'; import { AlbumArtistListSort, LibraryItem, SortOrder } from '/@/renderer/api/types'; +import { MultiSelect, NumberInput, SpinnerIcon, Switch, Text } from '/@/renderer/components'; import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query'; +import { useGenreList } from '/@/renderer/features/genres'; +import { AlbumListFilter, useListStoreActions } from '/@/renderer/store'; interface JellyfinAlbumFiltersProps { + customFilters?: Partial; disableArtistFilter?: boolean; - handleFilterChange: (filters: AlbumListFilter) => void; - id?: string; + onFilterChange: (filters: AlbumListFilter) => void; pageKey: string; serverId?: string; } export const JellyfinAlbumFilters = ({ + customFilters, disableArtistFilter, - handleFilterChange, + onFilterChange, pageKey, - id, serverId, }: JellyfinAlbumFiltersProps) => { - const filter = useAlbumListFilter({ id, key: pageKey }); + const filter = useListFilterByKey({ key: pageKey }); const { setFilter } = useListStoreActions(); // TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library @@ -45,6 +46,7 @@ export const JellyfinAlbumFilters = ({ label: 'Is favorited', onChange: (e: ChangeEvent) => { const updatedFilters = setFilter({ + customFilters, data: { _custom: { ...filter._custom, @@ -57,7 +59,7 @@ export const JellyfinAlbumFilters = ({ itemType: LibraryItem.ALBUM, key: pageKey, }) as AlbumListFilter; - handleFilterChange(updatedFilters); + onFilterChange(updatedFilters); }, value: filter._custom?.jellyfin?.IsFavorite, }, @@ -66,6 +68,7 @@ export const JellyfinAlbumFilters = ({ const handleMinYearFilter = debounce((e: number | string) => { if (typeof e === 'number' && (e < 1700 || e > 2300)) return; const updatedFilters = setFilter({ + customFilters, data: { _custom: { ...filter._custom, @@ -78,12 +81,13 @@ export const JellyfinAlbumFilters = ({ itemType: LibraryItem.ALBUM, key: pageKey, }) as AlbumListFilter; - handleFilterChange(updatedFilters); + onFilterChange(updatedFilters); }, 500); const handleMaxYearFilter = debounce((e: number | string) => { if (typeof e === 'number' && (e < 1700 || e > 2300)) return; const updatedFilters = setFilter({ + customFilters, data: { _custom: { ...filter._custom, @@ -96,12 +100,13 @@ export const JellyfinAlbumFilters = ({ itemType: LibraryItem.ALBUM, key: pageKey, }) as AlbumListFilter; - handleFilterChange(updatedFilters); + onFilterChange(updatedFilters); }, 500); const handleGenresFilter = debounce((e: string[] | undefined) => { const genreFilterString = e?.length ? e.join(',') : undefined; const updatedFilters = setFilter({ + customFilters, data: { _custom: { ...filter._custom, @@ -114,7 +119,7 @@ export const JellyfinAlbumFilters = ({ itemType: LibraryItem.ALBUM, key: pageKey, }) as AlbumListFilter; - handleFilterChange(updatedFilters); + onFilterChange(updatedFilters); }, 250); const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState(''); @@ -144,6 +149,7 @@ export const JellyfinAlbumFilters = ({ const handleAlbumArtistFilter = (e: string[] | null) => { const albumArtistFilterString = e?.length ? e.join(',') : undefined; const updatedFilters = setFilter({ + customFilters, data: { _custom: { ...filter._custom, @@ -156,7 +162,7 @@ export const JellyfinAlbumFilters = ({ itemType: LibraryItem.ALBUM, key: pageKey, }) as AlbumListFilter; - handleFilterChange(updatedFilters); + onFilterChange(updatedFilters); }; return ( diff --git a/src/renderer/features/albums/components/navidrome-album-filters.tsx b/src/renderer/features/albums/components/navidrome-album-filters.tsx index 43cddd0e..9cdacaa1 100644 --- a/src/renderer/features/albums/components/navidrome-album-filters.tsx +++ b/src/renderer/features/albums/components/navidrome-album-filters.tsx @@ -1,28 +1,28 @@ import { ChangeEvent, useMemo, useState } from 'react'; import { Divider, Group, Stack } from '@mantine/core'; import { NumberInput, Switch, Text, Select, SpinnerIcon } from '/@/renderer/components'; -import { AlbumListFilter, useAlbumListFilter, useListStoreActions } from '/@/renderer/store'; +import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store'; import debounce from 'lodash/debounce'; import { useGenreList } from '/@/renderer/features/genres'; import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query'; import { AlbumArtistListSort, LibraryItem, SortOrder } from '/@/renderer/api/types'; interface NavidromeAlbumFiltersProps { + customFilters?: Partial; disableArtistFilter?: boolean; - handleFilterChange: (filters: AlbumListFilter) => void; - id?: string; + onFilterChange: (filters: AlbumListFilter) => void; pageKey: string; serverId?: string; } export const NavidromeAlbumFilters = ({ - handleFilterChange, + customFilters, + onFilterChange, disableArtistFilter, pageKey, - id, serverId, }: NavidromeAlbumFiltersProps) => { - const filter = useAlbumListFilter({ id, key: pageKey }); + const { filter } = useListStoreByKey({ key: pageKey }); const { setFilter } = useListStoreActions(); const genreListQuery = useGenreList({ query: null, serverId }); @@ -37,6 +37,7 @@ export const NavidromeAlbumFilters = ({ const handleGenresFilter = debounce((e: string | null) => { const updatedFilters = setFilter({ + customFilters, data: { _custom: { ...filter._custom, @@ -47,9 +48,9 @@ export const NavidromeAlbumFilters = ({ }, }, itemType: LibraryItem.ALBUM, - key: 'album', + key: pageKey, }) as AlbumListFilter; - handleFilterChange(updatedFilters); + onFilterChange(updatedFilters); }, 250); const toggleFilters = [ @@ -57,6 +58,7 @@ export const NavidromeAlbumFilters = ({ label: 'Is rated', onChange: (e: ChangeEvent) => { const updatedFilters = setFilter({ + customFilters, data: { _custom: { ...filter._custom, @@ -69,7 +71,7 @@ export const NavidromeAlbumFilters = ({ itemType: LibraryItem.ALBUM, key: pageKey, }) as AlbumListFilter; - handleFilterChange(updatedFilters); + onFilterChange(updatedFilters); }, value: filter._custom?.navidrome?.has_rating, }, @@ -77,6 +79,7 @@ export const NavidromeAlbumFilters = ({ label: 'Is favorited', onChange: (e: ChangeEvent) => { const updatedFilters = setFilter({ + customFilters, data: { _custom: { ...filter._custom, @@ -89,7 +92,7 @@ export const NavidromeAlbumFilters = ({ itemType: LibraryItem.ALBUM, key: pageKey, }) as AlbumListFilter; - handleFilterChange(updatedFilters); + onFilterChange(updatedFilters); }, value: filter._custom?.navidrome?.starred, }, @@ -97,6 +100,7 @@ export const NavidromeAlbumFilters = ({ label: 'Is compilation', onChange: (e: ChangeEvent) => { const updatedFilters = setFilter({ + customFilters, data: { _custom: { ...filter._custom, @@ -109,7 +113,7 @@ export const NavidromeAlbumFilters = ({ itemType: LibraryItem.ALBUM, key: pageKey, }) as AlbumListFilter; - handleFilterChange(updatedFilters); + onFilterChange(updatedFilters); }, value: filter._custom?.navidrome?.compilation, }, @@ -117,6 +121,7 @@ export const NavidromeAlbumFilters = ({ label: 'Is recently played', onChange: (e: ChangeEvent) => { const updatedFilters = setFilter({ + customFilters, data: { _custom: { ...filter._custom, @@ -129,7 +134,7 @@ export const NavidromeAlbumFilters = ({ itemType: LibraryItem.ALBUM, key: pageKey, }) as AlbumListFilter; - handleFilterChange(updatedFilters); + onFilterChange(updatedFilters); }, value: filter._custom?.navidrome?.recently_played, }, @@ -137,6 +142,7 @@ export const NavidromeAlbumFilters = ({ const handleYearFilter = debounce((e: number | string) => { const updatedFilters = setFilter({ + customFilters, data: { _custom: { navidrome: { @@ -149,7 +155,7 @@ export const NavidromeAlbumFilters = ({ itemType: LibraryItem.ALBUM, key: pageKey, }) as AlbumListFilter; - handleFilterChange(updatedFilters); + onFilterChange(updatedFilters); }, 500); const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState(''); @@ -191,7 +197,7 @@ export const NavidromeAlbumFilters = ({ itemType: LibraryItem.ALBUM, key: pageKey, }) as AlbumListFilter; - handleFilterChange(updatedFilters); + onFilterChange(updatedFilters); }; return ( diff --git a/src/renderer/features/albums/routes/album-list-route.tsx b/src/renderer/features/albums/routes/album-list-route.tsx index 5d819e4a..d80ade24 100644 --- a/src/renderer/features/albums/routes/album-list-route.tsx +++ b/src/renderer/features/albums/routes/album-list-route.tsx @@ -1,28 +1,39 @@ -import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; -import { AnimatedPage } from '/@/renderer/features/shared'; -import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header'; -import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content'; -import { useRef } from 'react'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; -import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query'; -import { generatePageKey, useAlbumListFilter, useCurrentServer } from '/@/renderer/store'; +import { useCallback, useMemo, useRef } from 'react'; import { useParams, useSearchParams } from 'react-router-dom'; -import { AlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; +import { api } from '/@/renderer/api'; +import { queryKeys } from '/@/renderer/api/query-keys'; +import { LibraryItem } from '/@/renderer/api/types'; +import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; +import { ListContext } from '/@/renderer/context/list-context'; +import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content'; +import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header'; +import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query'; +import { usePlayQueueAdd } from '/@/renderer/features/player'; +import { AnimatedPage } from '/@/renderer/features/shared'; +import { queryClient } from '/@/renderer/lib/react-query'; +import { useCurrentServer, useListFilterByKey } from '/@/renderer/store'; +import { Play } from '/@/renderer/types'; const AlbumListRoute = () => { const gridRef = useRef(null); const tableRef = useRef(null); const server = useCurrentServer(); - const [searchParams] = useSearchParams(); const { albumArtistId } = useParams(); + const pageKey = albumArtistId ? `albumArtistAlbum` : 'album'; + const handlePlayQueueAdd = usePlayQueueAdd(); - const pageKey = generatePageKey( - 'album', - albumArtistId ? `${albumArtistId}_${server?.id}` : undefined, - ); + const customFilters = useMemo(() => { + return { + ...(albumArtistId && { artistIds: [albumArtistId] }), + }; + }, [albumArtistId]); - const albumListFilter = useAlbumListFilter({ id: albumArtistId || undefined, key: pageKey }); + const albumListFilter = useListFilterByKey({ + filter: customFilters, + key: pageKey, + }); const itemCountCheck = useAlbumList({ options: { @@ -42,9 +53,43 @@ const AlbumListRoute = () => { ? undefined : itemCountCheck.data?.totalRecordCount; + const handlePlay = useCallback( + async (args: { initialSongId?: string; playType: Play }) => { + if (!itemCount || itemCount === 0) return; + const { playType } = args; + const query = { + startIndex: 0, + ...albumListFilter, + ...customFilters, + }; + const queryKey = queryKeys.albums.list(server?.id || '', query); + + const albumListRes = await queryClient.fetchQuery({ + queryFn: ({ signal }) => { + return api.controller.getAlbumList({ + apiClientProps: { server, signal }, + query, + }); + }, + queryKey, + }); + + const albumIds = albumListRes?.items?.map((a) => a.id) || []; + + handlePlayQueueAdd?.({ + byItemType: { + id: albumIds, + type: LibraryItem.ALBUM, + }, + playType, + }); + }, + [albumListFilter, customFilters, handlePlayQueueAdd, itemCount, server], + ); + return ( - + { itemCount={itemCount} tableRef={tableRef} /> - + ); }; diff --git a/src/renderer/store/list.store.ts b/src/renderer/store/list.store.ts index 3e8f5a11..e883ca5e 100644 --- a/src/renderer/store/list.store.ts +++ b/src/renderer/store/list.store.ts @@ -55,6 +55,7 @@ export interface ListState { item: { album: ListItemProps; albumArtist: ListItemProps; + albumArtistAlbum: ListItemProps; albumArtistSong: ListItemProps; albumDetail: ListItemProps; playlist: ListItemProps; @@ -380,6 +381,47 @@ export const useListStore = create()( scrollOffset: 0, }, }, + albumArtistAlbum: { + display: ListDisplayType.POSTER, + filter: { + sortBy: AlbumListSort.RECENTLY_ADDED, + sortOrder: SortOrder.DESC, + }, + grid: { itemsPerRow: 5, scrollOffset: 0 }, + table: { + autoFit: true, + columns: [ + { + column: TableColumn.ROW_INDEX, + width: 50, + }, + { + column: TableColumn.TITLE_COMBINED, + width: 500, + }, + { + column: TableColumn.DURATION, + width: 100, + }, + { + column: TableColumn.ALBUM_ARTIST, + width: 300, + }, + { + column: TableColumn.YEAR, + width: 100, + }, + ], + pagination: { + currentPage: 1, + itemsPerPage: 100, + totalItems: 1, + totalPages: 1, + }, + rowHeight: 60, + scrollOffset: 0, + }, + }, albumArtistSong: { display: ListDisplayType.TABLE, filter: { @@ -553,69 +595,6 @@ export const useListFilterByKey = (args: { filter?: Partial; k ); }; -export const useAlbumListStore = (args?: { id?: string; key?: string }) => - useListStore((state) => { - const detail = args?.key ? state.detail[args.key] : undefined; - - return { - ...state.item.album, - filter: { - ...state.item.album.filter, - ...detail?.filter, - }, - grid: { - ...state.item.album.grid, - ...detail?.grid, - }, - table: { - ...state.item.album.table, - ...detail?.table, - }, - }; - }, shallow); - -export const useSongListStore = (args?: { id?: string; key?: string }) => - useListStore((state) => { - const detail = args?.key ? state.detail[args.key] : undefined; - - return { - ...state.item.song, - filter: { - ...state.item.song.filter, - ...detail?.filter, - }, - grid: { - ...state.item.song.grid, - ...detail?.grid, - }, - table: { - ...state.item.song.table, - ...detail?.table, - }, - }; - }, shallow); - -export const usePlaylistListStore = (args?: { key?: string }) => - useListStore((state) => { - const detail = args?.key ? state.detail[args.key] : undefined; - - return { - ...state.item.playlist, - filter: { - ...state.item.playlist.filter, - ...detail?.filter, - }, - grid: { - ...state.item.playlist.grid, - ...detail?.grid, - }, - table: { - ...state.item.playlist.table, - ...detail?.table, - }, - }; - }, shallow); - export const useAlbumListFilter = (args: { id?: string; key?: string }) => useListStore((state) => { return state._actions.getFilter({