From ae292e3a5fac69b63946cd2e249aea7aab65115d Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sun, 5 Mar 2023 18:28:26 -0800 Subject: [PATCH] Begin normalizing list stores --- src/renderer/api/navidrome.api.ts | 1 + src/renderer/api/types.ts | 1 + .../virtual-table/table-pagination.tsx | 17 +- .../albums/components/album-list-content.tsx | 118 +++-- .../components/album-list-header-filters.tsx | 154 +++--- .../albums/components/album-list-header.tsx | 33 +- .../components/jellyfin-album-filters.tsx | 75 ++- .../components/navidrome-album-filters.tsx | 98 ++-- .../albums/context/album-list-context.tsx | 11 + .../albums/routes/album-list-route.tsx | 71 +-- .../components/jellyfin-song-filters.tsx | 74 ++- .../components/navidrome-song-filters.tsx | 51 +- .../songs/components/song-list-content.tsx | 90 ++-- .../components/song-list-header-filters.tsx | 141 ++--- .../songs/components/song-list-header.tsx | 24 +- .../songs/context/song-list-context.tsx | 16 + .../features/songs/routes/song-list-route.tsx | 59 +-- src/renderer/store/album.store.ts | 138 ----- src/renderer/store/auth.store.ts | 16 +- src/renderer/store/index.ts | 5 +- src/renderer/store/list.store.ts | 482 ++++++++++++++++++ src/renderer/store/song.store.ts | 146 ------ 22 files changed, 1057 insertions(+), 764 deletions(-) create mode 100644 src/renderer/features/albums/context/album-list-context.tsx create mode 100644 src/renderer/features/songs/context/song-list-context.tsx delete mode 100644 src/renderer/store/album.store.ts create mode 100644 src/renderer/store/list.store.ts delete mode 100644 src/renderer/store/song.store.ts diff --git a/src/renderer/api/navidrome.api.ts b/src/renderer/api/navidrome.api.ts index e74de20b..6f67628b 100644 --- a/src/renderer/api/navidrome.api.ts +++ b/src/renderer/api/navidrome.api.ts @@ -281,6 +281,7 @@ const getAlbumList = async (args: AlbumListArgs): Promise => { _order: sortOrderMap.navidrome[query.sortOrder], _sort: albumListSortMap.navidrome[query.sortBy], _start: query.startIndex, + artist_id: query.artistIds?.[0], name: query.searchTerm, ...query.ndParams, }; diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index 2f1357ca..98bb20c2 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -343,6 +343,7 @@ export enum AlbumListSort { } export type AlbumListQuery = { + artistIds?: string[]; jfParams?: { albumArtistIds?: string; artistIds?: string; diff --git a/src/renderer/components/virtual-table/table-pagination.tsx b/src/renderer/components/virtual-table/table-pagination.tsx index d067dc9a..014f1258 100644 --- a/src/renderer/components/virtual-table/table-pagination.tsx +++ b/src/renderer/components/virtual-table/table-pagination.tsx @@ -12,21 +12,21 @@ import { Popover } from '/@/renderer/components/popover'; import { Text } from '/@/renderer/components/text'; import { useContainerQuery } from '/@/renderer/hooks'; import { TablePagination as TablePaginationType } from '/@/renderer/types'; +import { ListKey } from '/@/renderer/store'; interface TablePaginationProps { - id?: string; + pageKey: ListKey; pagination: TablePaginationType; setIdPagination?: (id: string, pagination: Partial) => void; - setPagination?: (pagination: Partial) => void; + setPagination?: (args: { data: Partial; key: ListKey }) => void; tableRef: MutableRefObject; } export const TablePagination = ({ - id, + pageKey, tableRef, pagination, setPagination, - setIdPagination, }: TablePaginationProps) => { const [isGoToPageOpen, handlers] = useDisclosure(false); const containerQuery = useContainerQuery(); @@ -40,8 +40,7 @@ export const TablePagination = ({ const handlePagination = (index: number) => { const newPage = index - 1; tableRef.current?.api.paginationGoToPage(newPage); - setPagination?.({ currentPage: newPage }); - setIdPagination?.(id || '', { currentPage: newPage }); + setPagination?.({ data: { currentPage: newPage }, key: pageKey }); }; const handleGoSubmit = goToForm.onSubmit((values) => { @@ -52,8 +51,7 @@ export const TablePagination = ({ const newPage = values.pageNumber - 1; tableRef.current?.api.paginationGoToPage(newPage); - setPagination?.({ currentPage: newPage }); - setIdPagination?.(id || '', { currentPage: newPage }); + setPagination?.({ data: { currentPage: newPage }, key: pageKey }); }); const currentPageStartIndex = pagination.currentPage * pagination.itemsPerPage + 1; @@ -103,7 +101,6 @@ export const TablePagination = ({ trapFocus opened={isGoToPageOpen} position="bottom-start" - transition="fade" onClose={() => handlers.close()} > @@ -142,10 +139,10 @@ export const TablePagination = ({ noWrap $hideDividers={!containerQuery.isSm} boundaries={1} - page={pagination.currentPage + 1} radius="sm" siblings={containerQuery.isMd ? 2 : containerQuery.isSm ? 1 : 0} total={pagination.totalPages - 1} + value={pagination.currentPage + 1} onChange={handlePagination} /> diff --git a/src/renderer/features/albums/components/album-list-content.tsx b/src/renderer/features/albums/components/album-list-content.tsx index 7a494f7a..4bfced04 100644 --- a/src/renderer/features/albums/components/album-list-content.tsx +++ b/src/renderer/features/albums/components/album-list-content.tsx @@ -19,13 +19,11 @@ import { Album, AlbumListQuery, AlbumListSort, LibraryItem } from '/@/renderer/a import { useQueryClient } from '@tanstack/react-query'; import { useCurrentServer, - useSetAlbumStore, useAlbumListStore, - useAlbumTablePagination, - useSetAlbumTable, - useSetAlbumTablePagination, useAlbumListItemData, AlbumListFilter, + useListStoreActions, + useAlbumListFilter, } from '/@/renderer/store'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { @@ -43,6 +41,7 @@ import { ALBUM_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/cont import { generatePath, useNavigate } from 'react-router'; import { usePlayQueueAdd } from '/@/renderer/features/player'; import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared'; +import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; interface AlbumListContentProps { customFilters?: Partial; @@ -60,23 +59,18 @@ export const AlbumListContent = ({ const queryClient = useQueryClient(); const navigate = useNavigate(); const server = useCurrentServer(); - const page = useAlbumListStore(); - const setPage = useSetAlbumStore(); const handlePlayQueueAdd = usePlayQueueAdd(); const { itemData, setItemData } = useAlbumListItemData(); const [localItemData, setLocalItemData] = useState([]); - const pagination = useAlbumTablePagination(); - const setPagination = useSetAlbumTablePagination(); - const setTable = useSetAlbumTable(); + const { id, pageKey } = useAlbumListContext(); + const filter = useAlbumListFilter({ id, key: pageKey }); + const { setTable, setTablePagination, setGrid } = useListStoreActions(); + const { table, grid, display } = useAlbumListStore(); + const isPaginationEnabled = display === ListDisplayType.TABLE_PAGINATED; - const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED; - - const columnDefs: ColDef[] = useMemo( - () => getColumnDefs(page.table.columns), - [page.table.columns], - ); + const columnDefs: ColDef[] = useMemo(() => getColumnDefs(table.columns), [table.columns]); const onTableReady = useCallback( (params: GridReadyEvent) => { @@ -88,18 +82,20 @@ export const AlbumListContent = ({ const query: AlbumListQuery = { limit, startIndex, - ...page.filter, + ...filter, ...customFilters, jfParams: { - ...page.filter.jfParams, + ...filter.jfParams, ...customFilters?.jfParams, }, ndParams: { - ...page.filter.ndParams, + ...filter.ndParams, ...customFilters?.ndParams, }, }; + console.log('query', query); + const queryKey = queryKeys.albums.list(server?.id || '', query); const albumsRes = await queryClient.fetchQuery( @@ -121,10 +117,10 @@ export const AlbumListContent = ({ params.api.setDatasource(dataSource); if (!customFilters) { - params.api.ensureIndexVisible(page.table.scrollOffset || 0, 'top'); + params.api.ensureIndexVisible(table.scrollOffset || 0, 'top'); } }, - [customFilters, page.filter, page.table.scrollOffset, queryClient, server], + [customFilters, filter, table.scrollOffset, queryClient, server], ); const onTablePaginationChanged = useCallback( @@ -133,19 +129,28 @@ export const AlbumListContent = ({ try { // Scroll to top of page on pagination change - const currentPageStartIndex = pagination.currentPage * pagination.itemsPerPage; + const currentPageStartIndex = table.pagination.currentPage * table.pagination.itemsPerPage; event.api?.ensureIndexVisible(currentPageStartIndex, 'top'); } catch (err) { console.log(err); } - setPagination({ - itemsPerPage: event.api.paginationGetPageSize(), - totalItems: event.api.paginationGetRowCount(), - totalPages: event.api.paginationGetTotalPages() + 1, + setTablePagination({ + data: { + itemsPerPage: event.api.paginationGetPageSize(), + totalItems: event.api.paginationGetRowCount(), + totalPages: event.api.paginationGetTotalPages() + 1, + }, + key: pageKey, }); }, - [isPaginationEnabled, pagination.currentPage, pagination.itemsPerPage, setPagination], + [ + isPaginationEnabled, + setTablePagination, + pageKey, + table.pagination.currentPage, + table.pagination.itemsPerPage, + ], ); const handleTableColumnChange = useCallback(() => { @@ -154,7 +159,7 @@ export const AlbumListContent = ({ if (!columnsOrder) return; - const columnsInSettings = page.table.columns; + const columnsInSettings = table.columns; const updatedColumns = []; for (const column of columnsOrder) { const columnInSettings = columnsInSettings.find((c) => c.column === column.getColDef().colId); @@ -162,22 +167,22 @@ export const AlbumListContent = ({ if (columnInSettings) { updatedColumns.push({ ...columnInSettings, - ...(!page.table.autoFit && { + ...(!table.autoFit && { width: column.getColDef().width, }), }); } } - setTable({ columns: updatedColumns }); - }, [page.table.autoFit, page.table.columns, setTable, tableRef]); + setTable({ data: { columns: updatedColumns }, key: pageKey }); + }, [tableRef, table.columns, table.autoFit, setTable, pageKey]); const debouncedTableColumnChange = debounce(handleTableColumnChange, 200); const handleTableScroll = (e: BodyScrollEvent) => { if (customFilters) return; - const scrollOffset = Number((e.top / page.table.rowHeight).toFixed(0)); - setTable({ scrollOffset }); + const scrollOffset = Number((e.top / table.rowHeight).toFixed(0)); + setTable({ data: { scrollOffset }, key: pageKey }); }; const fetch = useCallback( @@ -185,14 +190,14 @@ export const AlbumListContent = ({ const query: AlbumListQuery = { limit: take, startIndex: skip, - ...page.filter, + ...filter, ...customFilters, jfParams: { - ...page.filter.jfParams, + ...filter.jfParams, ...customFilters?.jfParams, }, ndParams: { - ...page.filter.ndParams, + ...filter.ndParams, ...customFilters?.ndParams, }, }; @@ -209,29 +214,21 @@ export const AlbumListContent = ({ return api.normalize.albumList(albums, server); }, - [customFilters, page.filter, queryClient, server], + [customFilters, filter, queryClient, server], ); const handleGridScroll = useCallback( (e: ListOnScrollProps) => { if (customFilters) return; - setPage({ - list: { - ...page, - grid: { - ...page.grid, - scrollOffset: e.scrollOffset, - }, - }, - }); + setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey }); }, - [customFilters, page, setPage], + [customFilters, pageKey, setGrid], ); const cardRows = useMemo(() => { const rows: CardRow[] = [ALBUM_CARD_ROWS.name]; - switch (page.filter.sortBy) { + switch (filter.sortBy) { case AlbumListSort.ALBUM_ARTIST: rows.push(ALBUM_CARD_ROWS.albumArtists); rows.push(ALBUM_CARD_ROWS.releaseYear); @@ -289,7 +286,7 @@ export const AlbumListContent = ({ } return rows; - }, [page.filter.sortBy]); + }, [filter.sortBy]); const handleContextMenu = useHandleTableContextMenu(LibraryItem.ALBUM, ALBUM_CONTEXT_MENU_ITEMS); @@ -326,24 +323,24 @@ export const AlbumListContent = ({ return ( <> - {page.display === ListDisplayType.CARD || page.display === ListDisplayType.POSTER ? ( + {display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? ( {({ height, width }) => ( <> data.data.id} infiniteInitialRowCount={itemCount || 100} pagination={isPaginationEnabled} paginationAutoPageSize={isPaginationEnabled} - paginationPageSize={page.table.pagination.itemsPerPage || 100} + paginationPageSize={table.pagination.itemsPerPage || 100} rowBuffer={20} - rowHeight={page.table.rowHeight || 40} + rowHeight={table.rowHeight || 40} rowModelType="infinite" onBodyScrollEnd={handleTableScroll} onCellContextMenu={handleContextMenu} @@ -393,10 +390,11 @@ export const AlbumListContent = ({ initial={false} mode="wait" > - {page.display === ListDisplayType.TABLE_PAGINATED && ( + {display === ListDisplayType.TABLE_PAGINATED && ( )} 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 0b44f0a0..e9f77b85 100644 --- a/src/renderer/features/albums/components/album-list-header-filters.tsx +++ b/src/renderer/features/albums/components/album-list-header-filters.tsx @@ -29,21 +29,19 @@ import { Text, VirtualInfiniteGridRef, } from '/@/renderer/components'; -import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters'; -import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters'; import { useContainerQuery } from '/@/renderer/hooks'; import { AlbumListFilter, useAlbumListStore, useCurrentServer, - useSetAlbumFilters, - useSetAlbumStore, - useSetAlbumTable, - useSetAlbumTablePagination, + useListStoreActions, } from '/@/renderer/store'; import { ServerType, Play, ListDisplayType, TableColumn } from '/@/renderer/types'; import { 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'; const FILTERS = { jellyfin: [ @@ -94,25 +92,21 @@ export const AlbumListHeaderFilters = ({ itemCount, }: AlbumListHeaderFiltersProps) => { const queryClient = useQueryClient(); + const { id, pageKey } = useAlbumListContext(); const server = useCurrentServer(); - - const setPage = useSetAlbumStore(); - const setFilter = useSetAlbumFilters(); - const page = useAlbumListStore(); - const filters = page.filter; + const { setFilter, setTablePagination, setTable, setGrid, setDisplayType, setTableColumns } = + useListStoreActions(); + const { display, filter, table, grid } = useAlbumListStore(); const cq = useContainerQuery(); const musicFoldersQuery = useMusicFolders(); - const setPagination = useSetAlbumTablePagination(); - const setTable = useSetAlbumTable(); - const sortByLabel = (server?.type && - FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filters.sortBy)?.name) || + FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filter.sortBy)?.name) || 'Unknown'; - const sortOrderLabel = ORDER.find((o) => o.value === filters.sortOrder)?.name || 'Unknown'; + const sortOrderLabel = ORDER.find((o) => o.value === filter.sortOrder)?.name || 'Unknown'; const fetch = useCallback( async (skip: number, take: number, filters: AlbumListFilter) => { @@ -151,10 +145,7 @@ export const AlbumListHeaderFilters = ({ const handleFilterChange = useCallback( async (filters: AlbumListFilter) => { - if ( - page.display === ListDisplayType.TABLE || - page.display === ListDisplayType.TABLE_PAGINATED - ) { + if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) { const dataSource: IDatasource = { getRows: async (params) => { const limit = params.endRow - params.startRow; @@ -197,8 +188,8 @@ export const AlbumListHeaderFilters = ({ tableRef.current?.api.purgeInfiniteCache(); tableRef.current?.api.ensureIndexVisible(0, 'top'); - if (page.display === ListDisplayType.TABLE_PAGINATED) { - setPagination({ currentPage: 0 }); + if (display === ListDisplayType.TABLE_PAGINATED) { + setTablePagination({ data: { currentPage: 0 }, key: 'album' }); } } else { gridRef.current?.scrollTo(0); @@ -213,7 +204,7 @@ export const AlbumListHeaderFilters = ({ gridRef.current?.setItemData(data.items); } }, - [page.display, tableRef, customFilters, server, queryClient, setPagination, gridRef, fetch], + [display, tableRef, customFilters, server, queryClient, setTablePagination, gridRef, fetch], ); const handleOpenFiltersModal = () => { @@ -224,11 +215,15 @@ export const AlbumListHeaderFilters = ({ ) : ( )} @@ -239,8 +234,8 @@ export const AlbumListHeaderFilters = ({ const handleRefresh = useCallback(() => { queryClient.invalidateQueries(queryKeys.albums.list(server?.id || '')); - handleFilterChange(filters); - }, [filters, handleFilterChange, queryClient, server?.id]); + handleFilterChange(filter); + }, [filter, handleFilterChange, queryClient, server?.id]); const handleSetSortBy = useCallback( (e: MouseEvent) => { @@ -251,9 +246,12 @@ export const AlbumListHeaderFilters = ({ )?.defaultOrder; const updatedFilters = setFilter({ - sortBy: e.currentTarget.value as AlbumListSort, - sortOrder: sortOrder || SortOrder.ASC, - }); + data: { + sortBy: e.currentTarget.value as AlbumListSort, + sortOrder: sortOrder || SortOrder.ASC, + }, + key: 'album', + }) as AlbumListFilter; handleFilterChange(updatedFilters); }, @@ -265,22 +263,31 @@ export const AlbumListHeaderFilters = ({ if (!e.currentTarget?.value) return; let updatedFilters = null; - if (e.currentTarget.value === String(page.filter.musicFolderId)) { - updatedFilters = setFilter({ musicFolderId: undefined }); + if (e.currentTarget.value === String(filter.musicFolderId)) { + updatedFilters = setFilter({ + data: { musicFolderId: undefined }, + key: 'album', + }) as AlbumListFilter; } else { - updatedFilters = setFilter({ musicFolderId: e.currentTarget.value }); + updatedFilters = setFilter({ + data: { musicFolderId: e.currentTarget.value }, + key: 'album', + }) as AlbumListFilter; } handleFilterChange(updatedFilters); }, - [handleFilterChange, page.filter.musicFolderId, setFilter], + [handleFilterChange, filter.musicFolderId, setFilter], ); const handleToggleSortOrder = useCallback(() => { - const newSortOrder = filters.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; - const updatedFilters = setFilter({ sortOrder: newSortOrder }); + const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; + const updatedFilters = setFilter({ + data: { sortOrder: newSortOrder }, + key: 'album', + }) as AlbumListFilter; handleFilterChange(updatedFilters); - }, [filters.sortOrder, handleFilterChange, setFilter]); + }, [filter.sortOrder, handleFilterChange, setFilter]); const handlePlayQueueAdd = usePlayQueueAdd(); @@ -289,14 +296,14 @@ export const AlbumListHeaderFilters = ({ const query = { startIndex: 0, - ...filters, + ...filter, ...customFilters, jfParams: { - ...filters.jfParams, + ...filter.jfParams, ...customFilters?.jfParams, }, ndParams: { - ...filters.ndParams, + ...filter.ndParams, ...customFilters?.ndParams, }, }; @@ -320,30 +327,28 @@ export const AlbumListHeaderFilters = ({ }; const handleItemSize = (e: number) => { - if ( - page.display === ListDisplayType.TABLE || - page.display === ListDisplayType.TABLE_PAGINATED - ) { - setTable({ rowHeight: e }); + if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) { + setTable({ data: { rowHeight: e }, key: 'album' }); } else { - setPage({ list: { ...page, grid: { ...page.grid, size: e } } }); + setGrid({ data: { size: e }, key: 'album' }); } }; const handleSetViewType = useCallback( (e: MouseEvent) => { if (!e.currentTarget?.value) return; - setPage({ list: { ...page, display: e.currentTarget.value as ListDisplayType } }); + setDisplayType({ data: e.currentTarget.value as ListDisplayType, key: 'album' }); }, - [page, setPage], + [setDisplayType], ); const handleTableColumns = (values: TableColumn[]) => { - const existingColumns = page.table.columns; + const existingColumns = table.columns; if (values.length === 0) { - return setTable({ - columns: [], + return setTableColumns({ + data: [], + key: 'album', }); } @@ -351,20 +356,20 @@ export const AlbumListHeaderFilters = ({ if (values.length > existingColumns.length) { const newColumn = { column: values[values.length - 1], width: 100 }; - setTable({ columns: [...existingColumns, newColumn] }); + setTableColumns({ data: [...existingColumns, newColumn], key: 'album' }); } else { // If removing a column const removed = existingColumns.filter((column) => !values.includes(column.column)); const newColumns = existingColumns.filter((column) => !removed.includes(column)); - setTable({ columns: newColumns }); + setTableColumns({ data: newColumns, key: 'album' }); } return tableRef.current?.api.sizeColumnsToFit(); }; const handleAutoFitColumns = (e: ChangeEvent) => { - setTable({ autoFit: e.currentTarget.checked }); + setTable({ data: { autoFit: e.currentTarget.checked }, key: 'album' }); if (e.currentTarget.checked) { tableRef.current?.api.sizeColumnsToFit(); @@ -374,16 +379,16 @@ export const AlbumListHeaderFilters = ({ const isFilterApplied = useMemo(() => { const isNavidromeFilterApplied = server?.type === ServerType.NAVIDROME && - page.filter.ndParams && - Object.values(page.filter.ndParams).some((value) => value !== undefined); + filter.ndParams && + Object.values(filter.ndParams).some((value) => value !== undefined); const isJellyfinFilterApplied = server?.type === ServerType.JELLYFIN && - page.filter.jfParams && - Object.values(page.filter.jfParams).some((value) => value !== undefined); + filter.jfParams && + Object.values(filter.jfParams).some((value) => value !== undefined); return isNavidromeFilterApplied || isJellyfinFilterApplied; - }, [page.filter.jfParams, page.filter.ndParams, server?.type]); + }, [filter.jfParams, filter.ndParams, server?.type]); return ( @@ -404,14 +409,14 @@ export const AlbumListHeaderFilters = ({ - {FILTERS[server?.type as keyof typeof FILTERS].map((filter) => ( + {FILTERS[server?.type as keyof typeof FILTERS].map((f) => ( - {filter.name} + {f.name} ))} @@ -427,7 +432,7 @@ export const AlbumListHeaderFilters = ({ sortOrderLabel ) : ( <> - {filters.sortOrder === SortOrder.ASC ? ( + {filter.sortOrder === SortOrder.ASC ? ( ) : ( @@ -451,7 +456,7 @@ export const AlbumListHeaderFilters = ({ {musicFoldersQuery.data?.map((folder) => ( @@ -528,28 +533,28 @@ export const AlbumListHeaderFilters = ({ Display type Card Poster Table @@ -560,9 +565,9 @@ export const AlbumListHeaderFilters = ({ - {(page.display === ListDisplayType.TABLE || - page.display === ListDisplayType.TABLE_PAGINATED) && ( + {(display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) && ( <> Table Columns column.column)} + defaultValue={table?.columns.map((column) => column.column)} width={300} onChange={handleTableColumns} /> Auto Fit Columns diff --git a/src/renderer/features/albums/components/album-list-header.tsx b/src/renderer/features/albums/components/album-list-header.tsx index 0a94722f..821dbbef 100644 --- a/src/renderer/features/albums/components/album-list-header.tsx +++ b/src/renderer/features/albums/components/album-list-header.tsx @@ -22,8 +22,7 @@ import { AlbumListFilter, useAlbumListStore, useCurrentServer, - useSetAlbumFilters, - useSetAlbumTablePagination, + useListStoreActions, } from '/@/renderer/store'; import { ListDisplayType, Play } from '/@/renderer/types'; import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters'; @@ -47,12 +46,9 @@ export const AlbumListHeader = ({ }: AlbumListHeaderProps) => { const queryClient = useQueryClient(); const server = useCurrentServer(); - const setFilter = useSetAlbumFilters(); - const page = useAlbumListStore(); - const filters = page.filter; + const { setFilter, setTablePagination } = useListStoreActions(); const cq = useContainerQuery(); - - const setPagination = useSetAlbumTablePagination(); + const { filter, display } = useAlbumListStore(); const fetch = useCallback( async (skip: number, take: number, filters: AlbumListFilter) => { @@ -91,10 +87,7 @@ export const AlbumListHeader = ({ const handleFilterChange = useCallback( async (filters: AlbumListFilter) => { - if ( - page.display === ListDisplayType.TABLE || - page.display === ListDisplayType.TABLE_PAGINATED - ) { + if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) { const dataSource: IDatasource = { getRows: async (params) => { const limit = params.endRow - params.startRow; @@ -137,8 +130,8 @@ export const AlbumListHeader = ({ tableRef.current?.api.purgeInfiniteCache(); tableRef.current?.api.ensureIndexVisible(0, 'top'); - if (page.display === ListDisplayType.TABLE_PAGINATED) { - setPagination({ currentPage: 0 }); + if (display === ListDisplayType.TABLE_PAGINATED) { + setTablePagination({ data: { currentPage: 0 }, key: 'album' }); } } else { gridRef.current?.scrollTo(0); @@ -153,13 +146,13 @@ export const AlbumListHeader = ({ gridRef.current?.setItemData(data.items); } }, - [page.display, tableRef, customFilters, server, queryClient, setPagination, gridRef, fetch], + [display, tableRef, customFilters, server, queryClient, setTablePagination, gridRef, fetch], ); const handleSearch = debounce((e: ChangeEvent) => { - const previousSearchTerm = page.filter.searchTerm; + const previousSearchTerm = filter.searchTerm; const searchTerm = e.target.value === '' ? undefined : e.target.value; - const updatedFilters = setFilter({ searchTerm }); + const updatedFilters = setFilter({ data: { searchTerm }, key: 'album' }) as AlbumListFilter; if (previousSearchTerm !== searchTerm) handleFilterChange(updatedFilters); }, 500); @@ -171,14 +164,14 @@ export const AlbumListHeader = ({ const query = { startIndex: 0, - ...filters, + ...filter, ...customFilters, jfParams: { - ...filters.jfParams, + ...filter.jfParams, ...customFilters?.jfParams, }, ndParams: { - ...filters.ndParams, + ...filter.ndParams, ...customFilters?.ndParams, }, }; @@ -225,7 +218,7 @@ export const AlbumListHeader = ({ diff --git a/src/renderer/features/albums/components/jellyfin-album-filters.tsx b/src/renderer/features/albums/components/jellyfin-album-filters.tsx index 21041d3d..8451521f 100644 --- a/src/renderer/features/albums/components/jellyfin-album-filters.tsx +++ b/src/renderer/features/albums/components/jellyfin-album-filters.tsx @@ -1,7 +1,7 @@ import { ChangeEvent, useMemo, useState } from 'react'; import { Divider, Group, Stack } from '@mantine/core'; import { MultiSelect, NumberInput, SpinnerIcon, Switch, Text } from '/@/renderer/components'; -import { AlbumListFilter, useAlbumListStore, useSetAlbumFilters } from '/@/renderer/store'; +import { AlbumListFilter, useAlbumListFilter, useListStoreActions } from '/@/renderer/store'; import debounce from 'lodash/debounce'; import { useGenreList } from '/@/renderer/features/genres'; import { AlbumArtistListSort, SortOrder } from '/@/renderer/api/types'; @@ -10,14 +10,18 @@ import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-a interface JellyfinAlbumFiltersProps { disableArtistFilter?: boolean; handleFilterChange: (filters: AlbumListFilter) => void; + id?: string; + pageKey: string; } export const JellyfinAlbumFilters = ({ disableArtistFilter, handleFilterChange, + pageKey, + id, }: JellyfinAlbumFiltersProps) => { - const { filter } = useAlbumListStore(); - const setFilters = useSetAlbumFilters(); + const filter = useAlbumListFilter({ id, key: pageKey }); + const { setFilter } = useListStoreActions(); // TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library const genreListQuery = useGenreList(null); @@ -38,9 +42,16 @@ export const JellyfinAlbumFilters = ({ { label: 'Is favorited', onChange: (e: ChangeEvent) => { - const updatedFilters = setFilters({ - jfParams: { ...filter.jfParams, isFavorite: e.currentTarget.checked ? true : undefined }, - }); + const updatedFilters = setFilter({ + data: { + jfParams: { + ...filter.jfParams, + includeItemTypes: 'Audio', + isFavorite: e.currentTarget.checked ? true : undefined, + }, + }, + key: pageKey, + }) as AlbumListFilter; handleFilterChange(updatedFilters); }, value: filter.jfParams?.isFavorite, @@ -49,34 +60,43 @@ export const JellyfinAlbumFilters = ({ const handleMinYearFilter = debounce((e: number | string) => { if (typeof e === 'number' && (e < 1700 || e > 2300)) return; - const updatedFilters = setFilters({ - jfParams: { - ...filter.jfParams, - minYear: e === '' ? undefined : (e as number), + const updatedFilters = setFilter({ + data: { + jfParams: { + ...filter.jfParams, + minYear: e === '' ? undefined : (e as number), + }, }, - }); + key: pageKey, + }) as AlbumListFilter; handleFilterChange(updatedFilters); }, 500); const handleMaxYearFilter = debounce((e: number | string) => { if (typeof e === 'number' && (e < 1700 || e > 2300)) return; - const updatedFilters = setFilters({ - jfParams: { - ...filter.jfParams, - maxYear: e === '' ? undefined : (e as number), + const updatedFilters = setFilter({ + data: { + jfParams: { + ...filter.jfParams, + maxYear: e === '' ? undefined : (e as number), + }, }, - }); + key: pageKey, + }) as AlbumListFilter; handleFilterChange(updatedFilters); }, 500); const handleGenresFilter = debounce((e: string[] | undefined) => { const genreFilterString = e?.length ? e.join(',') : undefined; - const updatedFilters = setFilters({ - jfParams: { - ...filter.jfParams, - genreIds: genreFilterString, + const updatedFilters = setFilter({ + data: { + jfParams: { + ...filter.jfParams, + genreIds: genreFilterString, + }, }, - }); + key: pageKey, + }) as AlbumListFilter; handleFilterChange(updatedFilters); }, 250); @@ -105,12 +125,15 @@ export const JellyfinAlbumFilters = ({ const handleAlbumArtistFilter = (e: string[] | null) => { const albumArtistFilterString = e?.length ? e.join(',') : undefined; - const updatedFilters = setFilters({ - jfParams: { - ...filter.jfParams, - albumArtistIds: albumArtistFilterString, + const updatedFilters = setFilter({ + data: { + jfParams: { + ...filter.jfParams, + albumArtistIds: albumArtistFilterString, + }, }, - }); + key: pageKey, + }) as AlbumListFilter; handleFilterChange(updatedFilters); }; diff --git a/src/renderer/features/albums/components/navidrome-album-filters.tsx b/src/renderer/features/albums/components/navidrome-album-filters.tsx index d5c04dc3..c9e4308b 100644 --- a/src/renderer/features/albums/components/navidrome-album-filters.tsx +++ b/src/renderer/features/albums/components/navidrome-album-filters.tsx @@ -1,7 +1,7 @@ import { ChangeEvent, useMemo, useState } from 'react'; import { Divider, Group, Stack } from '@mantine/core'; import { NumberInput, Switch, Text, Select, SpinnerIcon } from '/@/renderer/components'; -import { AlbumListFilter, useAlbumListStore, useSetAlbumFilters } from '/@/renderer/store'; +import { AlbumListFilter, useAlbumListFilter, useListStoreActions } 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'; @@ -10,14 +10,20 @@ import { AlbumArtistListSort, SortOrder } from '/@/renderer/api/types'; interface NavidromeAlbumFiltersProps { disableArtistFilter?: boolean; handleFilterChange: (filters: AlbumListFilter) => void; + id?: string; + pageKey: string; } export const NavidromeAlbumFilters = ({ handleFilterChange, disableArtistFilter, + pageKey, + id, }: NavidromeAlbumFiltersProps) => { - const { filter } = useAlbumListStore(); - const setFilters = useSetAlbumFilters(); + const filter = useAlbumListFilter({ id, key: pageKey }); + const { setFilter } = useListStoreActions(); + + console.log('pageKey, id', pageKey, id); const genreListQuery = useGenreList(null); @@ -30,12 +36,15 @@ export const NavidromeAlbumFilters = ({ }, [genreListQuery.data]); const handleGenresFilter = debounce((e: string | null) => { - const updatedFilters = setFilters({ - ndParams: { - ...filter.ndParams, - genre_id: e || undefined, + const updatedFilters = setFilter({ + data: { + ndParams: { + ...filter.ndParams, + genre_id: e || undefined, + }, }, - }); + key: 'album', + }) as AlbumListFilter; handleFilterChange(updatedFilters); }, 250); @@ -43,9 +52,15 @@ export const NavidromeAlbumFilters = ({ { label: 'Is rated', onChange: (e: ChangeEvent) => { - const updatedFilters = setFilters({ - ndParams: { ...filter.ndParams, has_rating: e.currentTarget.checked ? true : undefined }, - }); + const updatedFilters = setFilter({ + data: { + ndParams: { + ...filter.ndParams, + has_rating: e.currentTarget.checked ? true : undefined, + }, + }, + key: pageKey, + }) as AlbumListFilter; handleFilterChange(updatedFilters); }, value: filter.ndParams?.has_rating, @@ -53,9 +68,13 @@ export const NavidromeAlbumFilters = ({ { label: 'Is favorited', onChange: (e: ChangeEvent) => { - const updatedFilters = setFilters({ - ndParams: { ...filter.ndParams, starred: e.currentTarget.checked ? true : undefined }, - }); + const updatedFilters = setFilter({ + data: { + ndParams: { ...filter.ndParams, starred: e.currentTarget.checked ? true : undefined }, + }, + key: pageKey, + }) as AlbumListFilter; + console.log('updatedFilters :>> ', updatedFilters); handleFilterChange(updatedFilters); }, value: filter.ndParams?.starred, @@ -63,9 +82,15 @@ export const NavidromeAlbumFilters = ({ { label: 'Is compilation', onChange: (e: ChangeEvent) => { - const updatedFilters = setFilters({ - ndParams: { ...filter.ndParams, compilation: e.currentTarget.checked ? true : undefined }, - }); + const updatedFilters = setFilter({ + data: { + ndParams: { + ...filter.ndParams, + compilation: e.currentTarget.checked ? true : undefined, + }, + }, + key: pageKey, + }) as AlbumListFilter; handleFilterChange(updatedFilters); }, value: filter.ndParams?.compilation, @@ -73,12 +98,15 @@ export const NavidromeAlbumFilters = ({ { label: 'Is recently played', onChange: (e: ChangeEvent) => { - const updatedFilters = setFilters({ - ndParams: { - ...filter.ndParams, - recently_played: e.currentTarget.checked ? true : undefined, + const updatedFilters = setFilter({ + data: { + ndParams: { + ...filter.ndParams, + recently_played: e.currentTarget.checked ? true : undefined, + }, }, - }); + key: pageKey, + }) as AlbumListFilter; handleFilterChange(updatedFilters); }, value: filter.ndParams?.recently_played, @@ -86,12 +114,15 @@ export const NavidromeAlbumFilters = ({ ]; const handleYearFilter = debounce((e: number | string) => { - const updatedFilters = setFilters({ - ndParams: { - ...filter.ndParams, - year: e === '' ? undefined : (e as number), + const updatedFilters = setFilter({ + data: { + ndParams: { + ...filter.ndParams, + year: e === '' ? undefined : (e as number), + }, }, - }); + key: pageKey, + }) as AlbumListFilter; handleFilterChange(updatedFilters); }, 500); @@ -120,12 +151,15 @@ export const NavidromeAlbumFilters = ({ }, [albumArtistListQuery?.data?.items]); const handleAlbumArtistFilter = (e: string | null) => { - const updatedFilters = setFilters({ - ndParams: { - ...filter.ndParams, - artist_id: e || undefined, + const updatedFilters = setFilter({ + data: { + ndParams: { + ...filter.ndParams, + artist_id: e || undefined, + }, }, - }); + key: pageKey, + }) as AlbumListFilter; handleFilterChange(updatedFilters); }; diff --git a/src/renderer/features/albums/context/album-list-context.tsx b/src/renderer/features/albums/context/album-list-context.tsx new file mode 100644 index 00000000..01b83905 --- /dev/null +++ b/src/renderer/features/albums/context/album-list-context.tsx @@ -0,0 +1,11 @@ +import { createContext, useContext } from 'react'; +import { ListKey } from '/@/renderer/store'; + +export const AlbumListContext = createContext<{ id?: string; pageKey: ListKey }>({ + pageKey: 'album', +}); + +export const useAlbumListContext = () => { + const ctxValue = useContext(AlbumListContext); + return ctxValue; +}; diff --git a/src/renderer/features/albums/routes/album-list-route.tsx b/src/renderer/features/albums/routes/album-list-route.tsx index dbdce5b8..b2fcc1cc 100644 --- a/src/renderer/features/albums/routes/album-list-route.tsx +++ b/src/renderer/features/albums/routes/album-list-route.tsx @@ -5,53 +5,34 @@ import { AlbumListContent } from '/@/renderer/features/albums/components/album-l 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 { useAlbumListFilters, useCurrentServer } from '/@/renderer/store'; -import { useSearchParams } from 'react-router-dom'; -import { AlbumListQuery, ServerType } from '/@/renderer/api/types'; +import { generatePageKey, useAlbumListFilter, useCurrentServer } from '/@/renderer/store'; +import { useParams, useSearchParams } from 'react-router-dom'; +import { AlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; const AlbumListRoute = () => { const gridRef = useRef(null); const tableRef = useRef(null); - const filters = useAlbumListFilters(); const server = useCurrentServer(); const [searchParams] = useSearchParams(); + const { albumArtistId } = useParams(); - const customFilters: Partial | undefined = searchParams.get('artistId') - ? { - jfParams: - server?.type === ServerType.JELLYFIN - ? { - artistIds: searchParams.get('artistId') as string, - } - : undefined, - ndParams: - server?.type === ServerType.NAVIDROME - ? { - artist_id: searchParams.get('artistId') as string, - } - : undefined, - } - : undefined; + const pageKey = generatePageKey( + 'album', + albumArtistId ? `${albumArtistId}_${server?.id}` : undefined, + ); + + const albumListFilter = useAlbumListFilter({ id: albumArtistId || undefined, key: pageKey }); const itemCountCheck = useAlbumList( { limit: 1, startIndex: 0, - ...filters, - ...customFilters, - jfParams: { - ...filters.jfParams, - ...customFilters?.jfParams, - }, - ndParams: { - ...filters.ndParams, - ...customFilters?.ndParams, - }, + ...albumListFilter, }, { - cacheTime: 1000 * 60 * 60 * 2, - staleTime: 1000 * 60 * 60 * 2, + cacheTime: 1000 * 60, + staleTime: 1000 * 60, }, ); @@ -62,19 +43,19 @@ const AlbumListRoute = () => { return ( - - + + + + ); }; diff --git a/src/renderer/features/songs/components/jellyfin-song-filters.tsx b/src/renderer/features/songs/components/jellyfin-song-filters.tsx index 9e81c862..69541986 100644 --- a/src/renderer/features/songs/components/jellyfin-song-filters.tsx +++ b/src/renderer/features/songs/components/jellyfin-song-filters.tsx @@ -1,17 +1,23 @@ import { ChangeEvent, useMemo } from 'react'; import { Divider, Group, Stack } from '@mantine/core'; import { MultiSelect, NumberInput, Switch, Text } from '/@/renderer/components'; -import { SongListFilter, useSetSongFilters, useSongListStore } from '/@/renderer/store'; +import { SongListFilter, useListStoreActions, useSongListFilter } from '/@/renderer/store'; import debounce from 'lodash/debounce'; import { useGenreList } from '/@/renderer/features/genres'; interface JellyfinSongFiltersProps { handleFilterChange: (filters: SongListFilter) => void; + id?: string; + pageKey: string; } -export const JellyfinSongFilters = ({ handleFilterChange }: JellyfinSongFiltersProps) => { - const { filter } = useSongListStore(); - const setFilters = useSetSongFilters(); +export const JellyfinSongFilters = ({ + id, + pageKey, + handleFilterChange, +}: JellyfinSongFiltersProps) => { + const { setFilter } = useListStoreActions(); + const filter = useSongListFilter({ id, key: pageKey }); // TODO - eventually replace with /items/filters endpoint to fetch genres and tags specific to the selected library const genreListQuery = useGenreList(null); @@ -32,13 +38,16 @@ export const JellyfinSongFilters = ({ handleFilterChange }: JellyfinSongFiltersP { label: 'Is favorited', onChange: (e: ChangeEvent) => { - const updatedFilters = setFilters({ - jfParams: { - ...filter.jfParams, - includeItemTypes: 'Audio', - isFavorite: e.currentTarget.checked ? true : undefined, + const updatedFilters = setFilter({ + data: { + jfParams: { + ...filter.jfParams, + includeItemTypes: 'Audio', + isFavorite: e.currentTarget.checked ? true : undefined, + }, }, - }); + key: pageKey, + }) as SongListFilter; handleFilterChange(updatedFilters); }, value: filter.jfParams?.isFavorite, @@ -47,37 +56,46 @@ export const JellyfinSongFilters = ({ handleFilterChange }: JellyfinSongFiltersP const handleMinYearFilter = debounce((e: number | string) => { if (typeof e === 'number' && (e < 1700 || e > 2300)) return; - const updatedFilters = setFilters({ - jfParams: { - ...filter.jfParams, - includeItemTypes: 'Audio', - minYear: e === '' ? undefined : (e as number), + const updatedFilters = setFilter({ + data: { + jfParams: { + ...filter.jfParams, + includeItemTypes: 'Audio', + minYear: e === '' ? undefined : (e as number), + }, }, - }); + key: pageKey, + }) as SongListFilter; handleFilterChange(updatedFilters); }, 500); const handleMaxYearFilter = debounce((e: number | string) => { if (typeof e === 'number' && (e < 1700 || e > 2300)) return; - const updatedFilters = setFilters({ - jfParams: { - ...filter.jfParams, - includeItemTypes: 'Audio', - maxYear: e === '' ? undefined : (e as number), + const updatedFilters = setFilter({ + data: { + jfParams: { + ...filter.jfParams, + includeItemTypes: 'Audio', + maxYear: e === '' ? undefined : (e as number), + }, }, - }); + key: pageKey, + }) as SongListFilter; handleFilterChange(updatedFilters); }, 500); const handleGenresFilter = debounce((e: string[] | undefined) => { const genreFilterString = e?.length ? e.join(',') : undefined; - const updatedFilters = setFilters({ - jfParams: { - ...filter.jfParams, - genreIds: genreFilterString, - includeItemTypes: 'Audio', + const updatedFilters = setFilter({ + data: { + jfParams: { + ...filter.jfParams, + genreIds: genreFilterString, + includeItemTypes: 'Audio', + }, }, - }); + key: pageKey, + }) as SongListFilter; handleFilterChange(updatedFilters); }, 250); diff --git a/src/renderer/features/songs/components/navidrome-song-filters.tsx b/src/renderer/features/songs/components/navidrome-song-filters.tsx index 21c6297d..cf3d37ec 100644 --- a/src/renderer/features/songs/components/navidrome-song-filters.tsx +++ b/src/renderer/features/songs/components/navidrome-song-filters.tsx @@ -1,17 +1,23 @@ import { ChangeEvent, useMemo } from 'react'; import { Divider, Group, Stack } from '@mantine/core'; import { NumberInput, Select, Switch, Text } from '/@/renderer/components'; -import { SongListFilter, useSetSongFilters, useSongListStore } from '/@/renderer/store'; +import { SongListFilter, useListStoreActions, useSongListFilter } from '/@/renderer/store'; import debounce from 'lodash/debounce'; import { useGenreList } from '/@/renderer/features/genres'; interface NavidromeSongFiltersProps { handleFilterChange: (filters: SongListFilter) => void; + id?: string; + pageKey: string; } -export const NavidromeSongFilters = ({ handleFilterChange }: NavidromeSongFiltersProps) => { - const { filter } = useSongListStore(); - const setFilters = useSetSongFilters(); +export const NavidromeSongFilters = ({ + handleFilterChange, + pageKey, + id, +}: NavidromeSongFiltersProps) => { + const { setFilter } = useListStoreActions(); + const filter = useSongListFilter({ id, key: pageKey }); const genreListQuery = useGenreList(null); @@ -24,12 +30,15 @@ export const NavidromeSongFilters = ({ handleFilterChange }: NavidromeSongFilter }, [genreListQuery.data]); const handleGenresFilter = debounce((e: string | null) => { - const updatedFilters = setFilters({ - ndParams: { - ...filter.ndParams, - genre_id: e || undefined, + const updatedFilters = setFilter({ + data: { + ndParams: { + ...filter.ndParams, + genre_id: e || undefined, + }, }, - }); + key: pageKey, + }) as SongListFilter; handleFilterChange(updatedFilters); }, 250); @@ -37,9 +46,14 @@ export const NavidromeSongFilters = ({ handleFilterChange }: NavidromeSongFilter { label: 'Is favorited', onChange: (e: ChangeEvent) => { - const updatedFilters = setFilters({ - ndParams: { ...filter.ndParams, starred: e.currentTarget.checked ? true : undefined }, - }); + const updatedFilters = setFilter({ + data: { + ndParams: { ...filter.ndParams, starred: e.currentTarget.checked ? true : undefined }, + }, + key: pageKey, + }) as SongListFilter; + + console.log('updatedFilters :>> ', updatedFilters); handleFilterChange(updatedFilters); }, value: filter.ndParams?.starred, @@ -47,12 +61,15 @@ export const NavidromeSongFilters = ({ handleFilterChange }: NavidromeSongFilter ]; const handleYearFilter = debounce((e: number | string) => { - const updatedFilters = setFilters({ - ndParams: { - ...filter.ndParams, - year: e === '' ? undefined : (e as number), + const updatedFilters = setFilter({ + data: { + ndParams: { + ...filter.ndParams, + year: e === '' ? undefined : (e as number), + }, }, - }); + key: pageKey, + }) as SongListFilter; handleFilterChange(updatedFilters); }, 500); diff --git a/src/renderer/features/songs/components/song-list-content.tsx b/src/renderer/features/songs/components/song-list-content.tsx index 15fb04c3..96f90f03 100644 --- a/src/renderer/features/songs/components/song-list-content.tsx +++ b/src/renderer/features/songs/components/song-list-content.tsx @@ -19,12 +19,10 @@ import { VirtualTable, } from '/@/renderer/components'; import { - SongListFilter, useCurrentServer, - useSetSongTable, - useSetSongTablePagination, + useListStoreActions, + useSongListFilter, useSongListStore, - useSongTablePagination, } from '/@/renderer/store'; import { ListDisplayType } from '/@/renderer/types'; import { AnimatePresence } from 'framer-motion'; @@ -34,30 +32,28 @@ import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/conte import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { LibraryItem, QueueSong, SongListQuery } from '/@/renderer/api/types'; import { usePlayQueueAdd } from '/@/renderer/features/player'; +import { useSongListContext } from '/@/renderer/features/songs/context/song-list-context'; interface SongListContentProps { - customFilters?: Partial; itemCount?: number; tableRef: MutableRefObject; } -export const SongListContent = ({ customFilters, itemCount, tableRef }: SongListContentProps) => { +export const SongListContent = ({ itemCount, tableRef }: SongListContentProps) => { const queryClient = useQueryClient(); const server = useCurrentServer(); - const page = useSongListStore(); - const pagination = useSongTablePagination(); - const setPagination = useSetSongTablePagination(); - const setTable = useSetSongTable(); + const { display, table } = useSongListStore(); + const { id, pageKey } = useSongListContext(); + const filter = useSongListFilter({ id, key: pageKey }); + + const { setTable, setTablePagination } = useListStoreActions(); const handlePlayQueueAdd = usePlayQueueAdd(); const playButtonBehavior = usePlayButtonBehavior(); - const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED; + const isPaginationEnabled = display === ListDisplayType.TABLE_PAGINATED; - const columnDefs: ColDef[] = useMemo( - () => getColumnDefs(page.table.columns), - [page.table.columns], - ); + const columnDefs: ColDef[] = useMemo(() => getColumnDefs(table.columns), [table.columns]); const onGridReady = useCallback( (params: GridReadyEvent) => { @@ -69,8 +65,7 @@ export const SongListContent = ({ customFilters, itemCount, tableRef }: SongList const query: SongListQuery = { limit, startIndex, - ...page.filter, - ...customFilters, + ...filter, }; const queryKey = queryKeys.songs.list(server?.id || '', query); @@ -93,11 +88,9 @@ export const SongListContent = ({ customFilters, itemCount, tableRef }: SongList }; params.api.setDatasource(dataSource); - if (!customFilters) { - params.api.ensureIndexVisible(page.table.scrollOffset, 'top'); - } + params.api.ensureIndexVisible(table.scrollOffset, 'top'); }, - [customFilters, page.filter, page.table.scrollOffset, queryClient, server], + [filter, table.scrollOffset, queryClient, server], ); const onPaginationChanged = useCallback( @@ -106,22 +99,31 @@ export const SongListContent = ({ customFilters, itemCount, tableRef }: SongList try { // Scroll to top of page on pagination change - const currentPageStartIndex = pagination.currentPage * pagination.itemsPerPage; + const currentPageStartIndex = table.pagination.currentPage * table.pagination.itemsPerPage; event.api?.ensureIndexVisible(currentPageStartIndex, 'top'); } catch (err) { console.log(err); } - setPagination({ - itemsPerPage: event.api.paginationGetPageSize(), - totalItems: event.api.paginationGetRowCount(), - totalPages: event.api.paginationGetTotalPages() + 1, + setTablePagination({ + data: { + itemsPerPage: event.api.paginationGetPageSize(), + totalItems: event.api.paginationGetRowCount(), + totalPages: event.api.paginationGetTotalPages() + 1, + }, + key: pageKey, }); }, - [isPaginationEnabled, pagination.currentPage, pagination.itemsPerPage, setPagination], + [ + isPaginationEnabled, + pageKey, + setTablePagination, + table.pagination.currentPage, + table.pagination.itemsPerPage, + ], ); const handleGridSizeChange = () => { - if (page.table.autoFit) { + if (table.autoFit) { tableRef?.current?.api.sizeColumnsToFit(); } }; @@ -132,7 +134,7 @@ export const SongListContent = ({ customFilters, itemCount, tableRef }: SongList if (!columnsOrder) return; - const columnsInSettings = page.table.columns; + const columnsInSettings = table.columns; const updatedColumns = []; for (const column of columnsOrder) { const columnInSettings = columnsInSettings.find((c) => c.column === column.getColDef().colId); @@ -140,22 +142,22 @@ export const SongListContent = ({ customFilters, itemCount, tableRef }: SongList if (columnInSettings) { updatedColumns.push({ ...columnInSettings, - ...(!page.table.autoFit && { + ...(!table.autoFit && { width: column.getActualWidth(), }), }); } } - setTable({ columns: updatedColumns }); - }, [page.table.autoFit, page.table.columns, setTable, tableRef]); + setTable({ data: { columns: updatedColumns }, key: pageKey }); + }, [tableRef, table.columns, table.autoFit, setTable, pageKey]); const debouncedColumnChange = debounce(handleColumnChange, 200); const handleScroll = (e: BodyScrollEvent) => { - if (customFilters) return; - const scrollOffset = Number((e.top / page.table.rowHeight).toFixed(0)); - setTable({ scrollOffset }); + if (id) return; + const scrollOffset = Number((e.top / table.rowHeight).toFixed(0)); + setTable({ data: { scrollOffset }, key: pageKey }); }; const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, SONG_CONTEXT_MENU_ITEMS); @@ -177,23 +179,20 @@ export const SongListContent = ({ customFilters, itemCount, tableRef }: SongList data.data.id} infiniteInitialRowCount={itemCount || 100} pagination={isPaginationEnabled} paginationAutoPageSize={isPaginationEnabled} - paginationPageSize={page.table.pagination.itemsPerPage || 100} + paginationPageSize={table.pagination.itemsPerPage || 100} rowBuffer={20} - rowHeight={page.table.rowHeight || 40} + rowHeight={table.rowHeight || 40} rowModelType="infinite" rowSelection="multiple" onBodyScrollEnd={handleScroll} @@ -211,10 +210,11 @@ export const SongListContent = ({ customFilters, itemCount, tableRef }: SongList initial={false} mode="wait" > - {page.display === ListDisplayType.TABLE_PAGINATED && ( + {display === ListDisplayType.TABLE_PAGINATED && ( )} diff --git a/src/renderer/features/songs/components/song-list-header-filters.tsx b/src/renderer/features/songs/components/song-list-header-filters.tsx index 440c09e1..022dd725 100644 --- a/src/renderer/features/songs/components/song-list-header-filters.tsx +++ b/src/renderer/features/songs/components/song-list-header-filters.tsx @@ -34,15 +34,14 @@ import { useContainerQuery } from '/@/renderer/hooks'; import { queryClient } from '/@/renderer/lib/react-query'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { - useCurrentServer, - useSongListStore, - useSetSongStore, - useSetSongFilters, - useSetSongTable, - useSetSongTablePagination, SongListFilter, + useCurrentServer, + useListStoreActions, + useSongListFilter, + useSongListStore, } from '/@/renderer/store'; import { ListDisplayType, ServerType, Play, TableColumn } from '/@/renderer/types'; +import { useSongListContext } from '/@/renderer/features/songs/context/song-list-context'; const FILTERS = { jellyfin: [ @@ -93,11 +92,11 @@ export const SongListHeaderFilters = ({ tableRef, }: SongListHeaderFiltersProps) => { const server = useCurrentServer(); - const page = useSongListStore(); - const setPage = useSetSongStore(); - const setFilter = useSetSongFilters(); - const setTable = useSetSongTable(); - const setPagination = useSetSongTablePagination(); + const { id, pageKey } = useSongListContext(); + const { display, table } = useSongListStore(); + const { setFilter, setTable, setTablePagination, setDisplayType } = useListStoreActions(); + const filter = useSongListFilter({ id, key: pageKey }); + const handlePlayQueueAdd = usePlayQueueAdd(); const cq = useContainerQuery(); @@ -106,11 +105,11 @@ export const SongListHeaderFilters = ({ const sortByLabel = (server?.type && (FILTERS[server.type as keyof typeof FILTERS] as { name: string; value: string }[]).find( - (f) => f.value === page.filter.sortBy, + (f) => f.value === filter.sortBy, )?.name) || 'Unknown'; - const sortOrderLabel = ORDER.find((s) => s.value === page.filter.sortOrder)?.name; + const sortOrderLabel = ORDER.find((s) => s.value === filter.sortOrder)?.name; const handleFilterChange = useCallback( async (filters?: SongListFilter) => { @@ -119,7 +118,7 @@ export const SongListHeaderFilters = ({ const limit = params.endRow - params.startRow; const startIndex = params.startRow; - const pageFilters = filters || page.filter; + const pageFilters = filters || filter; const query: SongListQuery = { limit, @@ -149,9 +148,9 @@ export const SongListHeaderFilters = ({ tableRef.current?.api.setDatasource(dataSource); tableRef.current?.api.purgeInfiniteCache(); tableRef.current?.api.ensureIndexVisible(0, 'top'); - setPagination({ currentPage: 0 }); + setTablePagination({ data: { currentPage: 0 }, key: pageKey }); }, - [customFilters, page.filter, server, setPagination, tableRef], + [customFilters, filter, pageKey, server, setTablePagination, tableRef], ); const handleSetSortBy = useCallback( @@ -163,13 +162,16 @@ export const SongListHeaderFilters = ({ )?.defaultOrder; const updatedFilters = setFilter({ - sortBy: e.currentTarget.value as SongListSort, - sortOrder: sortOrder || SortOrder.ASC, - }); + data: { + sortBy: e.currentTarget.value as SongListSort, + sortOrder: sortOrder || SortOrder.ASC, + }, + key: pageKey, + }) as SongListFilter; handleFilterChange(updatedFilters); }, - [handleFilterChange, server?.type, setFilter], + [handleFilterChange, pageKey, server?.type, setFilter], ); const handleSetMusicFolder = useCallback( @@ -177,45 +179,60 @@ export const SongListHeaderFilters = ({ if (!e.currentTarget?.value) return; let updatedFilters = null; - if (e.currentTarget.value === String(page.filter.musicFolderId)) { - updatedFilters = setFilter({ musicFolderId: undefined }); + if (e.currentTarget.value === String(filter.musicFolderId)) { + updatedFilters = setFilter({ + data: { musicFolderId: undefined }, + key: pageKey, + }) as SongListFilter; } else { - updatedFilters = setFilter({ musicFolderId: e.currentTarget.value }); + updatedFilters = setFilter({ + data: { musicFolderId: e.currentTarget.value }, + key: pageKey, + }) as SongListFilter; } handleFilterChange(updatedFilters); }, - [handleFilterChange, page.filter.musicFolderId, setFilter], + [filter.musicFolderId, handleFilterChange, setFilter, pageKey], ); const handleToggleSortOrder = useCallback(() => { - const newSortOrder = page.filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; - const updatedFilters = setFilter({ sortOrder: newSortOrder }); + const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; + const updatedFilters = setFilter({ + data: { sortOrder: newSortOrder }, + key: pageKey, + }) as SongListFilter; handleFilterChange(updatedFilters); - }, [page.filter.sortOrder, handleFilterChange, setFilter]); + }, [filter.sortOrder, handleFilterChange, pageKey, setFilter]); const handleSetViewType = useCallback( (e: MouseEvent) => { if (!e.currentTarget?.value) return; const display = e.currentTarget.value as ListDisplayType; - setPage({ list: { ...page, display: e.currentTarget.value as ListDisplayType } }); + setDisplayType({ + data: e.currentTarget.value as ListDisplayType, + key: pageKey, + }); if (display === ListDisplayType.TABLE) { tableRef.current?.api.paginationSetPageSize(tableRef.current.props.infiniteInitialRowCount); - setPagination({ currentPage: 0 }); + setTablePagination({ data: { currentPage: 0 }, key: pageKey }); } else if (display === ListDisplayType.TABLE_PAGINATED) { - setPagination({ currentPage: 0 }); + setTablePagination({ data: { currentPage: 0 }, key: pageKey }); } }, - [page, setPage, setPagination, tableRef], + [pageKey, setDisplayType, setTablePagination, tableRef], ); const handleTableColumns = (values: TableColumn[]) => { - const existingColumns = page.table.columns; + const existingColumns = table.columns; if (values.length === 0) { return setTable({ - columns: [], + data: { + columns: [], + }, + key: pageKey, }); } @@ -223,18 +240,18 @@ export const SongListHeaderFilters = ({ if (values.length > existingColumns.length) { const newColumn = { column: values[values.length - 1], width: 100 }; - return setTable({ columns: [...existingColumns, newColumn] }); + return setTable({ data: { columns: [...existingColumns, newColumn] }, key: pageKey }); } // If removing a column const removed = existingColumns.filter((column) => !values.includes(column.column)); const newColumns = existingColumns.filter((column) => !removed.includes(column)); - return setTable({ columns: newColumns }); + return setTable({ data: { columns: newColumns }, key: pageKey }); }; const handleAutoFitColumns = (e: ChangeEvent) => { - setTable({ autoFit: e.currentTarget.checked }); + setTable({ data: { autoFit: e.currentTarget.checked }, key: pageKey }); if (e.currentTarget.checked) { tableRef.current?.api.sizeColumnsToFit(); @@ -242,17 +259,17 @@ export const SongListHeaderFilters = ({ }; const handleRowHeight = (e: number) => { - setTable({ rowHeight: e }); + setTable({ data: { rowHeight: e }, key: pageKey }); }; const handleRefresh = () => { queryClient.invalidateQueries(queryKeys.songs.list(server?.id || '')); - handleFilterChange(page.filter); + handleFilterChange(filter); }; const handlePlay = async (play: Play) => { if (!itemCount || itemCount === 0) return; - const query: SongListQuery = { startIndex: 0, ...page.filter, ...customFilters }; + const query: SongListQuery = { startIndex: 0, ...filter, ...customFilters }; handlePlayQueueAdd?.({ byItemType: { @@ -268,9 +285,17 @@ export const SongListHeaderFilters = ({ children: ( <> {server?.type === ServerType.NAVIDROME ? ( - + ) : ( - + )} ), @@ -281,18 +306,18 @@ export const SongListHeaderFilters = ({ const isFilterApplied = useMemo(() => { const isNavidromeFilterApplied = server?.type === ServerType.NAVIDROME && - page.filter.ndParams && - Object.values(page.filter.ndParams).some((value) => value !== undefined); + filter.ndParams && + Object.values(filter.ndParams).some((value) => value !== undefined); const isJellyfinFilterApplied = server?.type === ServerType.JELLYFIN && - page.filter.jfParams && - Object.values(page.filter.jfParams) + filter.jfParams && + Object.values(filter.jfParams) .filter((value) => value !== 'Audio') // Don't account for includeItemTypes: Audio .some((value) => value !== undefined); return isNavidromeFilterApplied || isJellyfinFilterApplied; - }, [page.filter.jfParams, page.filter.ndParams, server?.type]); + }, [filter.jfParams, filter.ndParams, server?.type]); return ( @@ -313,14 +338,14 @@ export const SongListHeaderFilters = ({ - {FILTERS[server?.type as keyof typeof FILTERS].map((filter) => ( + {FILTERS[server?.type as keyof typeof FILTERS].map((f) => ( - {filter.name} + {f.name} ))} @@ -336,7 +361,7 @@ export const SongListHeaderFilters = ({ sortOrderLabel ) : ( <> - {page.filter.sortOrder === SortOrder.ASC ? ( + {filter.sortOrder === SortOrder.ASC ? ( ) : ( @@ -360,7 +385,7 @@ export const SongListHeaderFilters = ({ {musicFoldersQuery.data?.map((folder) => ( @@ -437,14 +462,14 @@ export const SongListHeaderFilters = ({ Display type Table @@ -454,7 +479,7 @@ export const SongListHeaderFilters = ({ Item Size column.column)} + defaultValue={table?.columns.map((column) => column.column)} width={300} onChange={handleTableColumns} /> Auto Fit Columns diff --git a/src/renderer/features/songs/components/song-list-header.tsx b/src/renderer/features/songs/components/song-list-header.tsx index 095bcf1f..ada291e0 100644 --- a/src/renderer/features/songs/components/song-list-header.tsx +++ b/src/renderer/features/songs/components/song-list-header.tsx @@ -10,13 +10,13 @@ import { PageHeader, Paper, SearchInput, SpinnerIcon } from '/@/renderer/compone import { usePlayQueueAdd } from '/@/renderer/features/player'; import { LibraryHeaderBar } from '/@/renderer/features/shared'; import { SongListHeaderFilters } from '/@/renderer/features/songs/components/song-list-header-filters'; +import { useSongListContext } from '/@/renderer/features/songs/context/song-list-context'; import { useContainerQuery } from '/@/renderer/hooks'; import { queryClient } from '/@/renderer/lib/react-query'; import { SongListFilter, useCurrentServer, - useSetSongFilters, - useSetSongTablePagination, + useListStoreActions, useSongListStore, } from '/@/renderer/store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; @@ -36,9 +36,9 @@ export const SongListHeader = ({ tableRef, }: SongListHeaderProps) => { const server = useCurrentServer(); - const page = useSongListStore(); - const setFilter = useSetSongFilters(); - const setPagination = useSetSongTablePagination(); + const { pageKey } = useSongListContext(); + const { filter } = useSongListStore(); + const { setFilter, setTablePagination } = useListStoreActions(); const handlePlayQueueAdd = usePlayQueueAdd(); const cq = useContainerQuery(); @@ -49,7 +49,7 @@ export const SongListHeader = ({ const limit = params.endRow - params.startRow; const startIndex = params.startRow; - const pageFilters = filters || page.filter; + const pageFilters = filters || filter; const query: SongListQuery = { limit, @@ -79,15 +79,15 @@ export const SongListHeader = ({ tableRef.current?.api.setDatasource(dataSource); tableRef.current?.api.purgeInfiniteCache(); tableRef.current?.api.ensureIndexVisible(0, 'top'); - setPagination({ currentPage: 0 }); + setTablePagination({ data: { currentPage: 0 }, key: pageKey }); }, - [customFilters, page.filter, server, setPagination, tableRef], + [customFilters, filter, pageKey, server, setTablePagination, tableRef], ); const handleSearch = debounce((e: ChangeEvent) => { - const previousSearchTerm = page.filter.searchTerm; + const previousSearchTerm = filter.searchTerm; const searchTerm = e.target.value === '' ? undefined : e.target.value; - const updatedFilters = setFilter({ searchTerm }); + const updatedFilters = setFilter({ data: { searchTerm }, key: pageKey }) as SongListFilter; if (previousSearchTerm !== searchTerm) handleFilterChange(updatedFilters); }, 500); @@ -95,7 +95,7 @@ export const SongListHeader = ({ const handlePlay = async (play: Play) => { if (!itemCount || itemCount === 0) return; - const query: SongListQuery = { startIndex: 0, ...page.filter, ...customFilters }; + const query: SongListQuery = { startIndex: 0, ...filter, ...customFilters }; handlePlayQueueAdd?.({ byItemType: { @@ -130,7 +130,7 @@ export const SongListHeader = ({ diff --git a/src/renderer/features/songs/context/song-list-context.tsx b/src/renderer/features/songs/context/song-list-context.tsx new file mode 100644 index 00000000..94c9151c --- /dev/null +++ b/src/renderer/features/songs/context/song-list-context.tsx @@ -0,0 +1,16 @@ +import { createContext, useContext } from 'react'; +import { ListKey } from '/@/renderer/store'; + +interface SongListContextProps { + id?: string; + pageKey: ListKey; +} + +export const SongListContext = createContext({ + pageKey: 'song', +}); + +export const useSongListContext = () => { + const ctxValue = useContext(SongListContext); + return ctxValue; +}; diff --git a/src/renderer/features/songs/routes/song-list-route.tsx b/src/renderer/features/songs/routes/song-list-route.tsx index 0ef7f13a..f1e84156 100644 --- a/src/renderer/features/songs/routes/song-list-route.tsx +++ b/src/renderer/features/songs/routes/song-list-route.tsx @@ -1,44 +1,33 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { useRef } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import { SongListQuery } from '/@/renderer/api/types'; +import { useParams, useSearchParams } from 'react-router-dom'; import { AnimatedPage } from '/@/renderer/features/shared'; import { SongListContent } from '/@/renderer/features/songs/components/song-list-content'; import { SongListHeader } from '/@/renderer/features/songs/components/song-list-header'; +import { SongListContext } from '/@/renderer/features/songs/context/song-list-context'; import { useSongList } from '/@/renderer/features/songs/queries/song-list-query'; -import { useSongListFilters } from '/@/renderer/store'; +import { generatePageKey, useCurrentServer, useSongListFilter } from '/@/renderer/store'; const TrackListRoute = () => { const tableRef = useRef(null); - const filters = useSongListFilters(); - + const server = useCurrentServer(); const [searchParams] = useSearchParams(); + const { albumArtistId } = useParams(); + const pageKey = generatePageKey( + 'song', + albumArtistId ? `${albumArtistId}_${server?.id}` : undefined, + ); - const customFilters: Partial | undefined = searchParams.get('artistId') - ? { - artistIds: [searchParams.get('artistId') as string], - } - : undefined; - + const songListFilter = useSongListFilter({ id: albumArtistId, key: pageKey }); const itemCountCheck = useSongList( { limit: 1, startIndex: 0, - ...filters, - ...customFilters, - jfParams: { - ...customFilters?.jfParams, - ...filters.jfParams, - includeItemTypes: 'Audio', - }, - ndParams: { - ...customFilters?.ndParams, - ...filters.ndParams, - }, + ...songListFilter, }, { - cacheTime: 1000 * 60 * 60 * 2, - staleTime: 1000 * 60 * 60 * 2, + cacheTime: 1000 * 60, + staleTime: 1000 * 60, }, ); @@ -49,17 +38,17 @@ const TrackListRoute = () => { return ( - - + + + + ); }; diff --git a/src/renderer/store/album.store.ts b/src/renderer/store/album.store.ts deleted file mode 100644 index 96793b79..00000000 --- a/src/renderer/store/album.store.ts +++ /dev/null @@ -1,138 +0,0 @@ -import merge from 'lodash/merge'; -import create from 'zustand'; -import { devtools, persist } from 'zustand/middleware'; -import { immer } from 'zustand/middleware/immer'; -import { AlbumListArgs, AlbumListSort, SortOrder } from '/@/renderer/api/types'; -import { DataTableProps } from '/@/renderer/store/settings.store'; -import { ListDisplayType, TableColumn, TablePagination } from '/@/renderer/types'; - -type TableProps = { - pagination: TablePagination; - scrollOffset: number; -} & DataTableProps; - -type ListProps = { - display: ListDisplayType; - filter: T; - grid: { - scrollOffset: number; - size: number; - }; - table: TableProps; -}; - -export type AlbumListFilter = Omit; - -export interface AlbumState { - list: ListProps; -} - -export interface AlbumSlice extends AlbumState { - actions: { - setFilters: (data: Partial) => AlbumListFilter; - setStore: (data: Partial) => void; - setTable: (data: Partial) => void; - setTablePagination: (data: Partial) => void; - }; -} - -export const useAlbumStore = create()( - persist( - devtools( - immer((set, get) => ({ - actions: { - setFilters: (data) => { - set((state) => { - state.list.filter = { ...state.list.filter, ...data }; - }); - - return get().list.filter; - }, - setStore: (data) => { - set({ ...get(), ...data }); - }, - setTable: (data) => { - set((state) => { - state.list.table = { ...state.list.table, ...data }; - }); - }, - setTablePagination: (data) => { - set((state) => { - state.list.table.pagination = { ...state.list.table.pagination, ...data }; - }); - }, - }, - list: { - display: ListDisplayType.CARD, - filter: { - musicFolderId: undefined, - sortBy: AlbumListSort.RECENTLY_ADDED, - sortOrder: SortOrder.ASC, - }, - grid: { - scrollOffset: 0, - size: 50, - }, - 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, - }, - }, - })), - { name: 'store_album' }, - ), - { - merge: (persistedState, currentState) => { - return merge(currentState, persistedState); - }, - name: 'store_album', - version: 1, - }, - ), -); - -export const useAlbumStoreActions = () => useAlbumStore((state) => state.actions); - -export const useSetAlbumStore = () => useAlbumStore((state) => state.actions.setStore); - -export const useSetAlbumFilters = () => useAlbumStore((state) => state.actions.setFilters); - -export const useAlbumListStore = () => useAlbumStore((state) => state.list); - -export const useAlbumListFilters = () => useAlbumStore((state) => state.list.filter); - -export const useAlbumTablePagination = () => useAlbumStore((state) => state.list.table.pagination); - -export const useSetAlbumTablePagination = () => - useAlbumStore((state) => state.actions.setTablePagination); - -export const useSetAlbumTable = () => useAlbumStore((state) => state.actions.setTable); diff --git a/src/renderer/store/auth.store.ts b/src/renderer/store/auth.store.ts index d63d76ab..4ecdf99e 100644 --- a/src/renderer/store/auth.store.ts +++ b/src/renderer/store/auth.store.ts @@ -3,11 +3,9 @@ import { nanoid } from 'nanoid/non-secure'; import create from 'zustand'; import { devtools, persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; -import { AlbumListSort, SongListSort, SortOrder } from '/@/renderer/api/types'; import { useAlbumArtistListDataStore } from '/@/renderer/store/album-artist-list-data.store'; import { useAlbumListDataStore } from '/@/renderer/store/album-list-data.store'; -import { useAlbumStore } from '/@/renderer/store/album.store'; -import { useSongStore } from '/@/renderer/store/song.store'; +import { useListStore } from '/@/renderer/store/list.store'; import { ServerListItem } from '/@/renderer/types'; export interface AuthState { @@ -52,16 +50,8 @@ export const useAuthStore = create()( state.currentServer = server; if (server) { - useAlbumStore.getState().actions.setFilters({ - musicFolderId: undefined, - sortBy: AlbumListSort.RECENTLY_ADDED, - sortOrder: SortOrder.DESC, - }); - useSongStore.getState().actions.setFilters({ - musicFolderId: undefined, - sortBy: SongListSort.RECENTLY_ADDED, - sortOrder: SortOrder.DESC, - }); + // Reset list filters + useListStore.getState()._actions.resetFilter(); // Reset persisted grid list stores useAlbumListDataStore.getState().actions.setItemData([]); diff --git a/src/renderer/store/index.ts b/src/renderer/store/index.ts index 41f54385..bbc51963 100644 --- a/src/renderer/store/index.ts +++ b/src/renderer/store/index.ts @@ -1,9 +1,6 @@ export * from './auth.store'; export * from './player.store'; export * from './app.store'; -export * from './album.store'; -export * from './song.store'; -export * from './album-artist.store'; -export * from './playlist.store'; +export * from './list.store'; export * from './album-list-data.store'; export * from './album-artist-list-data.store'; diff --git a/src/renderer/store/list.store.ts b/src/renderer/store/list.store.ts new file mode 100644 index 00000000..004095f3 --- /dev/null +++ b/src/renderer/store/list.store.ts @@ -0,0 +1,482 @@ +import merge from 'lodash/merge'; +import create from 'zustand'; +import { devtools, persist } from 'zustand/middleware'; +import { immer } from 'zustand/middleware/immer'; +import shallow from 'zustand/shallow'; +import { + AlbumArtistListArgs, + AlbumArtistListSort, + AlbumListArgs, + AlbumListSort, + PlaylistListArgs, + PlaylistListSort, + SongListArgs, + SongListSort, + SortOrder, +} from '/@/renderer/api/types'; +import { DataTableProps, PersistedTableColumn } from '/@/renderer/store/settings.store'; +import { ListDisplayType, TableColumn, TablePagination } from '/@/renderer/types'; + +export const generatePageKey = (page: string, id?: string) => { + return id ? `${page}_${id}` : page; +}; + +export type AlbumListFilter = Omit; +export type SongListFilter = Omit; +export type AlbumArtistListFilter = Omit; +export type PlaylistListFilter = Omit; + +export type ListKey = keyof ListState['item'] | string; + +type FilterType = AlbumListFilter | SongListFilter | AlbumArtistListFilter | PlaylistListFilter; + +type ListTableProps = { + pagination: TablePagination; + scrollOffset: number; +} & DataTableProps; + +type ListGridProps = { + scrollOffset?: number; + size?: number; +}; + +type ItemProps = { + display: ListDisplayType; + filter: TFilter; + grid?: ListGridProps; + table: ListTableProps; +}; + +export interface ListState { + detail: { + [key: string]: Omit, 'display' | 'grid'>; + }; + item: { + album: ItemProps; + albumArtist: ItemProps; + albumDetail: ItemProps; + playlist: ItemProps; + song: ItemProps; + }; +} + +type DeterministicArgs = { key: ListKey }; + +export interface ListSlice extends ListState { + _actions: { + getFilter: (args: { id?: string; key?: string }) => FilterType; + resetFilter: () => void; + setDisplayType: (args: { data: ListDisplayType } & DeterministicArgs) => void; + setFilter: (args: { data: Partial } & DeterministicArgs) => FilterType; + setGrid: (args: { data: Partial } & DeterministicArgs) => void; + setStore: (data: Partial) => void; + setTable: (args: { data: Partial } & DeterministicArgs) => void; + setTableColumns: (args: { data: PersistedTableColumn[] } & DeterministicArgs) => void; + setTablePagination: (args: { data: Partial } & DeterministicArgs) => void; + }; +} + +export const useListStore = create()( + persist( + devtools( + immer((set, get) => ({ + _actions: { + getFilter: (args) => { + const state = get(); + + if (args.id && args.key) { + return { + artistIds: [args.id], + ...state.item.song.filter, + ...state.detail[args.key]?.filter, + jfParams: { + ...state.detail[args.key]?.filter.jfParams, + includeItemTypes: 'Audio', + }, + ndParams: { + ...state.detail[args.key]?.filter.ndParams, + }, + }; + } + + if (args.key) { + return state.item[args.key as keyof ListState['item']].filter; + } + + return state.item.song.filter; + }, + resetFilter: () => { + set((state) => { + state.item.album.filter = { + musicFolderId: undefined, + sortBy: AlbumListSort.RECENTLY_ADDED, + sortOrder: SortOrder.DESC, + } as AlbumListFilter; + + state.item.song.filter = { + musicFolderId: undefined, + sortBy: SongListSort.RECENTLY_ADDED, + sortOrder: SortOrder.DESC, + } as SongListFilter; + }); + }, + setDisplayType: (args) => { + set((state) => { + const [page] = args.key.split('_'); + state.item[page as keyof ListState['item']].display = args.data; + }); + }, + setFilter: (args) => { + const [, id] = args.key.split('_'); + console.log('args', args); + + set((state) => { + if (id) { + if (!state.detail[args.key]) { + state.detail[args.key] = { + filter: {} as FilterType, + table: { + pagination: { + currentPage: 1, + itemsPerPage: 100, + totalItems: 0, + totalPages: 0, + }, + } as ListTableProps, + }; + } + + state.detail[args.key].filter = { + ...state.detail[args.key as keyof ListState['item']].filter, + ...args.data, + } as FilterType; + } else { + state.item[args.key as keyof ListState['item']].filter = { + ...state.item[args.key as keyof ListState['item']].filter, + ...args.data, + } as FilterType; + } + }); + + return get()._actions.getFilter({ id, key: args.key }); + }, + setGrid: (args) => { + set((state) => { + if (state.item[args.key as keyof ListState['item']].grid) { + state.item[args.key as keyof ListState['item']].grid = { + ...state.item[args.key as keyof ListState['item']]?.grid, + ...args.data, + }; + } + }); + }, + setStore: (data) => { + set({ ...get(), ...data }); + }, + setTable: (args) => { + set((state) => { + const [page, id] = args.key.split('_'); + + if (id) { + if (!state.detail[args.key]) { + state.detail[args.key] = { + filter: {} as FilterType, + table: { + pagination: { + currentPage: 1, + itemsPerPage: 100, + totalItems: 0, + totalPages: 0, + }, + } as ListTableProps, + }; + } + + if (!state.detail[args.key]?.table) { + state.detail[args.key].table = { + ...state.item[page as keyof ListState['item']].table, + pagination: { + currentPage: 1, + itemsPerPage: 100, + totalItems: 0, + totalPages: 0, + }, + }; + } + + if (!state.detail[args.key]?.filter) { + state.detail[args.key].filter = { + ...state.item[page as keyof ListState['item']].filter, + jfParams: {}, + ndParams: {}, + }; + } + } else { + state.item[page as keyof ListState['item']].table = { + ...state.item[page as keyof ListState['item']].table, + ...args.data, + }; + } + }); + }, + setTableColumns: (args) => { + set((state) => { + state.item[args.key as keyof ListState['item']].table.columns = { + ...state.item[args.key as keyof ListState['item']].table.columns, + ...args.data, + }; + }); + }, + setTablePagination: (args) => { + set((state) => { + const [, id] = args.key.split('_'); + + if (id) { + if (!state.detail[args.key]) { + state.detail[args.key] = { + filter: {} as FilterType, + table: { + pagination: { + currentPage: 1, + itemsPerPage: 100, + totalItems: 0, + totalPages: 0, + }, + } as ListTableProps, + }; + } + + state.detail[args.key as keyof ListState['item']].table.pagination = { + ...state.detail[args.key as keyof ListState['item']].table.pagination, + ...args.data, + }; + } else { + state.item[args.key as keyof ListState['item']].table.pagination = { + ...state.item[args.key as keyof ListState['item']].table.pagination, + ...args.data, + }; + } + }); + }, + }, + detail: {}, + item: { + album: { + display: ListDisplayType.CARD, + filter: { + sortBy: AlbumListSort.RECENTLY_ADDED, + sortOrder: SortOrder.DESC, + }, + grid: { scrollOffset: 0, size: 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, + }, + }, + albumArtist: { + display: ListDisplayType.CARD, + filter: { + sortBy: AlbumArtistListSort.NAME, + sortOrder: SortOrder.DESC, + }, + grid: { scrollOffset: 0, size: 0 }, + table: { + autoFit: true, + columns: [ + { + column: TableColumn.ROW_INDEX, + width: 50, + }, + { + column: TableColumn.TITLE_COMBINED, + width: 500, + }, + ], + pagination: { + currentPage: 1, + itemsPerPage: 100, + totalItems: 1, + totalPages: 1, + }, + rowHeight: 60, + scrollOffset: 0, + }, + }, + albumDetail: { + display: ListDisplayType.TABLE, + filter: { + sortBy: SongListSort.ALBUM, + sortOrder: SortOrder.ASC, + }, + table: { + autoFit: true, + columns: [], + followCurrentSong: false, + pagination: { + currentPage: 1, + itemsPerPage: 100, + totalItems: 1, + totalPages: 1, + }, + rowHeight: 60, + scrollOffset: 0, + }, + }, + playlist: { + display: ListDisplayType.CARD, + filter: { + sortBy: PlaylistListSort.NAME, + sortOrder: SortOrder.DESC, + }, + grid: { scrollOffset: 0, size: 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, + width: 500, + }, + ], + pagination: { + currentPage: 1, + itemsPerPage: 100, + totalItems: 1, + totalPages: 1, + }, + rowHeight: 60, + scrollOffset: 0, + }, + }, + song: { + display: ListDisplayType.CARD, + filter: { + sortBy: SongListSort.RECENTLY_ADDED, + sortOrder: SortOrder.DESC, + }, + grid: { scrollOffset: 0, size: 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, + width: 300, + }, + { + column: TableColumn.ARTIST, + width: 100, + }, + { + column: TableColumn.YEAR, + width: 100, + }, + { + column: TableColumn.DATE_ADDED, + width: 100, + }, + { + column: TableColumn.PLAY_COUNT, + width: 100, + }, + ], + pagination: { + currentPage: 1, + itemsPerPage: 100, + totalItems: 1, + totalPages: 1, + }, + rowHeight: 60, + scrollOffset: 0, + }, + }, + }, + })), + { name: 'store_list' }, + ), + { + merge: (persistedState, currentState) => { + return merge(currentState, persistedState); + }, + name: 'store_list', + partialize: (state) => { + return Object.fromEntries( + Object.entries(state).filter(([key]) => !['detail'].includes(key)), + ); + }, + version: 1, + }, + ), +); + +export const useListStoreActions = () => useListStore((state) => state._actions); + +export const useAlbumListStore = () => useListStore((state) => state.item.album, shallow); + +export const useAlbumArtistListStore = () => + useListStore((state) => state.item.albumArtist, shallow); + +export const useSongListStore = () => useListStore((state) => state.item.song, shallow); + +export const useSongListFilter = (args: { id?: string; key?: string }) => + useListStore((state) => { + return state._actions.getFilter({ id: args.id, key: args.key }) as SongListFilter; + }, shallow); + +export const useAlbumListFilter = (args: { id?: string; key?: string }) => + useListStore((state) => { + return state._actions.getFilter({ id: args.id, key: args.key }) as AlbumListFilter; + }, shallow); + +export const useListDetail = (key: string) => useListStore((state) => state.detail[key], shallow); diff --git a/src/renderer/store/song.store.ts b/src/renderer/store/song.store.ts deleted file mode 100644 index 94c66567..00000000 --- a/src/renderer/store/song.store.ts +++ /dev/null @@ -1,146 +0,0 @@ -import merge from 'lodash/merge'; -import create from 'zustand'; -import { devtools, persist } from 'zustand/middleware'; -import { immer } from 'zustand/middleware/immer'; -import { SongListArgs, SongListSort, SortOrder } from '/@/renderer/api/types'; -import { DataTableProps } from '/@/renderer/store/settings.store'; -import { ListDisplayType, TableColumn, TablePagination } from '/@/renderer/types'; - -type TableProps = { - pagination: TablePagination; - scrollOffset: number; -} & DataTableProps; - -type ListProps = { - display: ListDisplayType; - filter: T; - table: TableProps; -}; - -export type SongListFilter = Omit; - -interface SongState { - list: ListProps; -} - -export interface SongSlice extends SongState { - actions: { - setFilters: (data: Partial) => SongListFilter; - setStore: (data: Partial) => void; - setTable: (data: Partial) => void; - setTablePagination: (data: Partial) => void; - }; -} - -export const useSongStore = create()( - persist( - devtools( - immer((set, get) => ({ - actions: { - setFilters: (data) => { - set((state) => { - state.list.filter = { ...state.list.filter, ...data }; - }); - - return get().list.filter; - }, - setStore: (data) => { - set({ ...get(), ...data }); - }, - setTable: (data) => { - set((state) => { - state.list.table = { ...state.list.table, ...data }; - }); - }, - setTablePagination: (data) => { - set((state) => { - state.list.table.pagination = { ...state.list.table.pagination, ...data }; - }); - }, - }, - list: { - display: ListDisplayType.TABLE, - filter: { - musicFolderId: undefined, - sortBy: SongListSort.RECENTLY_ADDED, - sortOrder: SortOrder.ASC, - }, - table: { - autoFit: true, - columns: [ - { - column: TableColumn.ROW_INDEX, - width: 50, - }, - { - column: TableColumn.TITLE_COMBINED, - width: 500, - }, - { - column: TableColumn.DURATION, - width: 100, - }, - { - column: TableColumn.ALBUM, - width: 300, - }, - { - column: TableColumn.ARTIST, - width: 100, - }, - { - column: TableColumn.YEAR, - width: 100, - }, - { - column: TableColumn.DATE_ADDED, - width: 100, - }, - { - column: TableColumn.PLAY_COUNT, - width: 100, - }, - ], - pagination: { - currentPage: 1, - itemsPerPage: 100, - totalItems: 1, - totalPages: 1, - }, - rowHeight: 60, - scrollOffset: 0, - }, - }, - })), - { name: 'store_song' }, - ), - { - merge: (persistedState, currentState) => { - return merge(currentState, persistedState); - }, - name: 'store_song', - version: 1, - }, - ), -); - -export const useSongStoreActions = () => useSongStore((state) => state.actions); - -export const useSetSongStore = () => useSongStore((state) => state.actions.setStore); - -export const useSetSongFilters = () => useSongStore((state) => state.actions.setFilters); - -export const useSongFilters = () => { - return useSongStore((state) => [state.list.filter, state.actions.setFilters]); -}; - -export const useSongListFilters = () => useSongStore((state) => state.list.filter); - -export const useSongListStore = () => useSongStore((state) => state.list); - -export const useSongTablePagination = () => useSongStore((state) => state.list.table.pagination); - -export const useSetSongTablePagination = () => - useSongStore((state) => state.actions.setTablePagination); - -export const useSetSongTable = () => useSongStore((state) => state.actions.setTable);