diff --git a/src/renderer/features/playlists/components/playlist-list-content.tsx b/src/renderer/features/playlists/components/playlist-list-content.tsx index c639f650..8120ad64 100644 --- a/src/renderer/features/playlists/components/playlist-list-content.tsx +++ b/src/renderer/features/playlists/components/playlist-list-content.tsx @@ -1,233 +1,45 @@ -import { MutableRefObject, useCallback, useMemo } from 'react'; -import type { - BodyScrollEvent, - ColDef, - GridReadyEvent, - IDatasource, - PaginationChangedEvent, - RowDoubleClickedEvent, -} from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; -import { Stack } from '@mantine/core'; -import { useQueryClient } from '@tanstack/react-query'; -import { api } from '/@/renderer/api'; -import { queryKeys } from '/@/renderer/api/query-keys'; -import { - useCurrentServer, - usePlaylistListStore, - usePlaylistTablePagination, - useSetPlaylistTable, - useSetPlaylistTablePagination, -} from '/@/renderer/store'; +import { lazy, MutableRefObject, Suspense } from 'react'; +import { Spinner } from '/@/renderer/components'; +import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; +import { usePlaylistListStore } from '/@/renderer/store'; import { ListDisplayType } from '/@/renderer/types'; -import { AnimatePresence } from 'framer-motion'; -import debounce from 'lodash/debounce'; -import { useHandleTableContextMenu } from '/@/renderer/features/context-menu'; -import { PLAYLIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; -import { generatePath, useNavigate } from 'react-router'; -import { AppRoute } from '/@/renderer/router/routes'; -import { LibraryItem } from '/@/renderer/api/types'; -import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid'; -import { getColumnDefs, VirtualTable, TablePagination } from '/@/renderer/components/virtual-table'; + +const PlaylistListTableView = lazy(() => + import('/@/renderer/features/playlists/components/playlist-list-table-view').then((module) => ({ + default: module.PlaylistListTableView, + })), +); + +const PlaylistListGridView = lazy(() => + import('/@/renderer/features/playlists/components/playlist-list-grid-view').then((module) => ({ + default: module.PlaylistListGridView, + })), +); interface PlaylistListContentProps { + gridRef: MutableRefObject; itemCount?: number; tableRef: MutableRefObject; } -export const PlaylistListContent = ({ tableRef, itemCount }: PlaylistListContentProps) => { - const navigate = useNavigate(); - const queryClient = useQueryClient(); - const server = useCurrentServer(); - const page = usePlaylistListStore(); - - const pagination = usePlaylistTablePagination(); - const setPagination = useSetPlaylistTablePagination(); - const setTable = useSetPlaylistTable(); - - const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED; - - const columnDefs: ColDef[] = useMemo( - () => getColumnDefs(page.table.columns), - [page.table.columns], - ); - - const defaultColumnDefs: ColDef = useMemo(() => { - return { - lockPinned: true, - lockVisible: true, - resizable: true, - }; - }, []); - - const onGridReady = useCallback( - (params: GridReadyEvent) => { - const dataSource: IDatasource = { - getRows: async (params) => { - const limit = params.endRow - params.startRow; - const startIndex = params.startRow; - - const queryKey = queryKeys.playlists.list(server?.id || '', { - limit, - startIndex, - ...page.filter, - }); - - const playlistsRes = await queryClient.fetchQuery( - queryKey, - async ({ signal }) => - api.controller.getPlaylistList({ - apiClientProps: { - server, - signal, - }, - query: { - limit, - startIndex, - ...page.filter, - }, - }), - { cacheTime: 1000 * 60 * 1 }, - ); - - params.successCallback( - playlistsRes?.items || [], - playlistsRes?.totalRecordCount || 0, - ); - }, - rowCount: undefined, - }; - params.api.setDatasource(dataSource); - params.api.ensureIndexVisible(page.table.scrollOffset, 'top'); - }, - [page.filter, page.table.scrollOffset, queryClient, server], - ); - - const onPaginationChanged = useCallback( - (event: PaginationChangedEvent) => { - if (!isPaginationEnabled || !event.api) return; - - try { - // Scroll to top of page on pagination change - const currentPageStartIndex = pagination.currentPage * pagination.itemsPerPage; - event.api?.ensureIndexVisible(currentPageStartIndex, 'top'); - } catch (err) { - console.log(err); - } - - setPagination({ - data: { - itemsPerPage: event.api.paginationGetPageSize(), - totalItems: event.api.paginationGetRowCount(), - totalPages: event.api.paginationGetTotalPages() + 1, - }, - }); - }, - [isPaginationEnabled, pagination.currentPage, pagination.itemsPerPage, setPagination], - ); - - const handleGridSizeChange = () => { - if (page.table.autoFit) { - tableRef?.current?.api.sizeColumnsToFit(); - } - }; - - const handleColumnChange = useCallback(() => { - const { columnApi } = tableRef?.current || {}; - const columnsOrder = columnApi?.getAllGridColumns(); - - if (!columnsOrder) return; - - const columnsInSettings = page.table.columns; - const updatedColumns = []; - for (const column of columnsOrder) { - const columnInSettings = columnsInSettings.find( - (c) => c.column === column.getColDef().colId, - ); - - if (columnInSettings) { - updatedColumns.push({ - ...columnInSettings, - ...(!page.table.autoFit && { - width: column.getActualWidth(), - }), - }); - } - } - - setTable({ columns: updatedColumns }); - }, [page.table.autoFit, page.table.columns, setTable, tableRef]); - - const debouncedColumnChange = debounce(handleColumnChange, 200); - - const handleScroll = (e: BodyScrollEvent) => { - const scrollOffset = Number((e.top / page.table.rowHeight).toFixed(0)); - setTable({ scrollOffset }); - }; - - const handleContextMenu = useHandleTableContextMenu( - LibraryItem.PLAYLIST, - PLAYLIST_CONTEXT_MENU_ITEMS, - ); - - const handleRowDoubleClick = (e: RowDoubleClickedEvent) => { - if (!e.data) return; - navigate(generatePath(AppRoute.PLAYLISTS_DETAIL, { playlistId: e.data.id })); - }; +export const PlaylistListContent = ({ gridRef, tableRef, itemCount }: PlaylistListContentProps) => { + const { display } = usePlaylistListStore(); return ( - - - data.data.id} - infiniteInitialRowCount={itemCount || 100} - pagination={isPaginationEnabled} - paginationAutoPageSize={isPaginationEnabled} - paginationPageSize={page.table.pagination.itemsPerPage || 100} - rowBuffer={20} - rowHeight={page.table.rowHeight || 40} - rowModelType="infinite" - rowSelection="multiple" - onBodyScrollEnd={handleScroll} - onCellContextMenu={handleContextMenu} - onColumnMoved={handleColumnChange} - onColumnResized={debouncedColumnChange} - onGridReady={onGridReady} - onGridSizeChanged={handleGridSizeChange} - onPaginationChanged={onPaginationChanged} - onRowDoubleClicked={handleRowDoubleClick} + }> + {display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? ( + - - - {page.display === ListDisplayType.TABLE_PAGINATED && ( - - )} - - + ) : ( + + )} +
+ ); }; diff --git a/src/renderer/features/playlists/components/playlist-list-grid-view.tsx b/src/renderer/features/playlists/components/playlist-list-grid-view.tsx new file mode 100644 index 00000000..49fbfb29 --- /dev/null +++ b/src/renderer/features/playlists/components/playlist-list-grid-view.tsx @@ -0,0 +1,157 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { MutableRefObject, useCallback, useMemo } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { ListOnScrollProps } from 'react-window'; +import { usePlaylistGridStore, usePlaylistStoreActions } from '../../../store/playlist.store'; +import { controller } from '/@/renderer/api/controller'; +import { queryKeys } from '/@/renderer/api/query-keys'; +import { LibraryItem, Playlist, PlaylistListQuery, PlaylistListSort } from '/@/renderer/api/types'; +import { PLAYLIST_CARD_ROWS } from '/@/renderer/components'; +import { + VirtualGridAutoSizerContainer, + VirtualInfiniteGrid, + VirtualInfiniteGridRef, +} from '/@/renderer/components/virtual-grid'; +import { usePlayQueueAdd } from '/@/renderer/features/player'; +import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared'; +import { AppRoute } from '/@/renderer/router/routes'; +import { useCurrentServer, usePlaylistListStore } from '/@/renderer/store'; +import { CardRow, ListDisplayType } from '/@/renderer/types'; + +interface PlaylistListGridViewProps { + gridRef: MutableRefObject; + itemCount?: number; +} + +export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridViewProps) => { + const queryClient = useQueryClient(); + const server = useCurrentServer(); + const handlePlayQueueAdd = usePlayQueueAdd(); + const { display } = usePlaylistListStore(); + const grid = usePlaylistGridStore(); + const { setGrid } = usePlaylistStoreActions(); + const page = usePlaylistListStore(); + + const createFavoriteMutation = useCreateFavorite({}); + const deleteFavoriteMutation = useDeleteFavorite({}); + + const handleFavorite = (options: { + id: string[]; + isFavorite: boolean; + itemType: LibraryItem; + }) => { + const { id, itemType, isFavorite } = options; + if (isFavorite) { + deleteFavoriteMutation.mutate({ + query: { + id, + type: itemType, + }, + serverId: server?.id, + }); + } else { + createFavoriteMutation.mutate({ + query: { + id, + type: itemType, + }, + serverId: server?.id, + }); + } + }; + + const cardRows = useMemo(() => { + const rows: CardRow[] = [PLAYLIST_CARD_ROWS.name]; + + switch (page.filter.sortBy) { + case PlaylistListSort.DURATION: + rows.push(PLAYLIST_CARD_ROWS.duration); + break; + case PlaylistListSort.NAME: + rows.push(PLAYLIST_CARD_ROWS.songCount); + break; + case PlaylistListSort.SONG_COUNT: + rows.push(PLAYLIST_CARD_ROWS.songCount); + break; + case PlaylistListSort.OWNER: + rows.push(PLAYLIST_CARD_ROWS.owner); + break; + case PlaylistListSort.PUBLIC: + rows.push(PLAYLIST_CARD_ROWS.public); + break; + case PlaylistListSort.UPDATED_AT: + break; + } + + return rows; + }, [page.filter.sortBy]); + + const handleGridScroll = useCallback( + (e: ListOnScrollProps) => { + setGrid({ data: { scrollOffset: e.scrollOffset } }); + }, + [setGrid], + ); + + const fetch = useCallback( + async ({ skip, take }: { skip: number; take: number }) => { + if (!server) { + return []; + } + + const query: PlaylistListQuery = { + limit: take, + startIndex: skip, + ...page.filter, + _custom: {}, + }; + + const queryKey = queryKeys.playlists.list(server?.id || '', query); + + const playlists = await queryClient.fetchQuery(queryKey, async ({ signal }) => + controller.getPlaylistList({ + apiClientProps: { + server, + signal, + }, + query, + }), + ); + + return playlists; + }, + [page.filter, queryClient, server], + ); + + return ( + + + {({ height, width }) => ( + + )} + + + ); +}; diff --git a/src/renderer/features/playlists/components/playlist-list-header-filters.tsx b/src/renderer/features/playlists/components/playlist-list-header-filters.tsx index 66a16ce3..b3cb83d5 100644 --- a/src/renderer/features/playlists/components/playlist-list-header-filters.tsx +++ b/src/renderer/features/playlists/components/playlist-list-header-filters.tsx @@ -1,18 +1,20 @@ import { ChangeEvent, MutableRefObject, useCallback, MouseEvent } from 'react'; import { IDatasource } from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; -import { Flex, Stack, Group } from '@mantine/core'; +import { Flex, Stack, Group, Divider } from '@mantine/core'; import { useQueryClient } from '@tanstack/react-query'; -import { RiSortAsc, RiSortDesc, RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react-icons/ri'; +import { RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react-icons/ri'; import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; -import { SortOrder, PlaylistListSort } from '/@/renderer/api/types'; +import { SortOrder, PlaylistListSort, PlaylistListQuery } from '/@/renderer/api/types'; import { DropdownMenu, Text, Button, Slider, MultiSelect, Switch } from '/@/renderer/components'; import { useContainerQuery } from '/@/renderer/hooks'; import { PlaylistListFilter, useCurrentServer, + usePlaylistGridStore, usePlaylistListStore, + usePlaylistStoreActions, useSetPlaylistFilters, useSetPlaylistStore, useSetPlaylistTable, @@ -20,6 +22,8 @@ import { } from '/@/renderer/store'; import { ListDisplayType, TableColumn } from '/@/renderer/types'; import { PLAYLIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table'; +import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; +import { OrderToggleButton } from '/@/renderer/features/shared'; const FILTERS = { jellyfin: [ @@ -37,16 +41,15 @@ const FILTERS = { ], }; -const ORDER = [ - { name: 'Ascending', value: SortOrder.ASC }, - { name: 'Descending', value: SortOrder.DESC }, -]; - interface PlaylistListHeaderFiltersProps { + gridRef: MutableRefObject; tableRef: MutableRefObject; } -export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFiltersProps) => { +export const PlaylistListHeaderFilters = ({ + gridRef, + tableRef, +}: PlaylistListHeaderFiltersProps) => { const queryClient = useQueryClient(); const server = useCurrentServer(); const page = usePlaylistListStore(); @@ -54,8 +57,13 @@ export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFilter const setFilter = useSetPlaylistFilters(); const setTable = useSetPlaylistTable(); const setPagination = useSetPlaylistTablePagination(); + const grid = usePlaylistGridStore(); + const { setGrid } = usePlaylistStoreActions(); + const { display } = usePlaylistListStore(); const cq = useContainerQuery(); + const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER; + const sortByLabel = (server?.type && ( @@ -63,53 +71,92 @@ export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFilter ).find((f) => f.value === page.filter.sortBy)?.name) || 'Unknown'; - const sortOrderLabel = ORDER.find((s) => s.value === page.filter.sortOrder)?.name; + const fetch = useCallback( + async (skip: number, take: number, filters: PlaylistListFilter) => { + const query: PlaylistListQuery = { + _custom: { + jellyfin: { + ...filters._custom?.jellyfin, + }, + navidrome: { + ...filters._custom?.navidrome, + }, + }, + limit: take, + startIndex: skip, + ...filters, + }; + + const queryKey = queryKeys.playlists.list(server?.id || '', query); + + const playlists = await queryClient.fetchQuery(queryKey, async ({ signal }) => + api.controller.getPlaylistList({ + apiClientProps: { + server, + signal, + }, + query, + }), + ); + + return playlists; + }, + [queryClient, server], + ); const handleFilterChange = useCallback( async (filters?: PlaylistListFilter) => { - const dataSource: IDatasource = { - getRows: async (params) => { - const limit = params.endRow - params.startRow; - const startIndex = params.startRow; + if (isGrid) { + console.log('filter change', filters); + gridRef.current?.scrollTo(0); + gridRef.current?.resetLoadMoreItemsCache(); + const data = await fetch(0, 200, filters || page.filter); + 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 pageFilters = filters || page.filter; + const pageFilters = filters || page.filter; - const queryKey = queryKeys.playlists.list(server?.id || '', { - limit, - startIndex, - ...pageFilters, - }); + const queryKey = queryKeys.playlists.list(server?.id || '', { + limit, + startIndex, + ...pageFilters, + }); - const playlistsRes = await queryClient.fetchQuery( - queryKey, - async ({ signal }) => - api.controller.getPlaylistList({ - apiClientProps: { - server, - signal, - }, - query: { - limit, - startIndex, - ...pageFilters, - }, - }), - { cacheTime: 1000 * 60 * 1 }, - ); + const playlistsRes = await queryClient.fetchQuery( + queryKey, + async ({ signal }) => + api.controller.getPlaylistList({ + apiClientProps: { + server, + signal, + }, + query: { + limit, + startIndex, + ...pageFilters, + }, + }), + ); - params.successCallback( - playlistsRes?.items || [], - playlistsRes?.totalRecordCount || 0, - ); - }, - rowCount: undefined, - }; - tableRef.current?.api.setDatasource(dataSource); - tableRef.current?.api.purgeInfiniteCache(); - tableRef.current?.api.ensureIndexVisible(0, 'top'); - setPagination({ data: { currentPage: 0 } }); + params.successCallback( + playlistsRes?.items || [], + playlistsRes?.totalRecordCount || 0, + ); + }, + rowCount: undefined, + }; + tableRef.current?.api.setDatasource(dataSource); + tableRef.current?.api.purgeInfiniteCache(); + tableRef.current?.api.ensureIndexVisible(0, 'top'); + setPagination({ data: { currentPage: 0 } }); + } }, - [page.filter, queryClient, server, setPagination, tableRef], + [fetch, gridRef, isGrid, page.filter, queryClient, server, setPagination, tableRef], ); const handleSetSortBy = useCallback( @@ -186,12 +233,17 @@ export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFilter } }; - const handleRowHeight = (e: number) => { - setTable({ rowHeight: e }); + const handleItemSize = (e: number) => { + if (isGrid) { + setGrid({ data: { itemsPerRow: e } }); + } else { + setTable({ rowHeight: e }); + } }; const handleRefresh = () => { - tableRef?.current?.api?.purgeInfiniteCache(); + queryClient.invalidateQueries(queryKeys.playlists.list(server?.id || '', page.filter)); + handleFilterChange(page.filter); }; return ( @@ -225,25 +277,12 @@ export const PlaylistListHeaderFilters = ({ tableRef }: PlaylistListHeaderFilter ))} - + + + + - + ); diff --git a/src/renderer/features/playlists/components/playlist-list-table-view.tsx b/src/renderer/features/playlists/components/playlist-list-table-view.tsx new file mode 100644 index 00000000..fdae517d --- /dev/null +++ b/src/renderer/features/playlists/components/playlist-list-table-view.tsx @@ -0,0 +1,232 @@ +import { + ColDef, + GridReadyEvent, + IDatasource, + PaginationChangedEvent, + BodyScrollEvent, + RowDoubleClickedEvent, +} from '@ag-grid-community/core'; +import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; +import { Stack } from '@mantine/core'; +import { useQueryClient } from '@tanstack/react-query'; +import { AnimatePresence } from 'framer-motion'; +import debounce from 'lodash/debounce'; +import { useMemo, useCallback } from 'react'; +import { generatePath, useNavigate } from 'react-router'; +import { api } from '/@/renderer/api'; +import { queryKeys } from '/@/renderer/api/query-keys'; +import { LibraryItem } from '/@/renderer/api/types'; +import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid'; +import { getColumnDefs, TablePagination, VirtualTable } from '/@/renderer/components/virtual-table'; +import { useHandleTableContextMenu } from '/@/renderer/features/context-menu'; +import { PLAYLIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; +import { AppRoute } from '/@/renderer/router/routes'; +import { + usePlaylistTablePagination, + useSetPlaylistTablePagination, + useSetPlaylistTable, + useCurrentServer, + usePlaylistListStore, +} from '/@/renderer/store'; +import { ListDisplayType } from '/@/renderer/types'; + +interface PlaylistListTableViewProps { + itemCount?: number; + tableRef: React.MutableRefObject; +} + +export const PlaylistListTableView = ({ tableRef, itemCount }: PlaylistListTableViewProps) => { + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const server = useCurrentServer(); + const page = usePlaylistListStore(); + const pagination = usePlaylistTablePagination(); + const setPagination = useSetPlaylistTablePagination(); + const setTable = useSetPlaylistTable(); + + const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED; + + const columnDefs: ColDef[] = useMemo( + () => getColumnDefs(page.table.columns), + [page.table.columns], + ); + + const defaultColumnDefs: ColDef = useMemo(() => { + return { + lockPinned: true, + lockVisible: true, + resizable: true, + }; + }, []); + + const onGridReady = useCallback( + (params: GridReadyEvent) => { + const dataSource: IDatasource = { + getRows: async (params) => { + const limit = params.endRow - params.startRow; + const startIndex = params.startRow; + + const queryKey = queryKeys.playlists.list(server?.id || '', { + limit, + startIndex, + ...page.filter, + }); + + const playlistsRes = await queryClient.fetchQuery( + queryKey, + async ({ signal }) => + api.controller.getPlaylistList({ + apiClientProps: { + server, + signal, + }, + query: { + limit, + startIndex, + ...page.filter, + }, + }), + { cacheTime: 1000 * 60 * 1 }, + ); + + params.successCallback( + playlistsRes?.items || [], + playlistsRes?.totalRecordCount || 0, + ); + }, + rowCount: undefined, + }; + params.api.setDatasource(dataSource); + params.api.ensureIndexVisible(page.table.scrollOffset, 'top'); + }, + [page.filter, page.table.scrollOffset, queryClient, server], + ); + + const onPaginationChanged = useCallback( + (event: PaginationChangedEvent) => { + if (!isPaginationEnabled || !event.api) return; + + try { + // Scroll to top of page on pagination change + const currentPageStartIndex = pagination.currentPage * pagination.itemsPerPage; + event.api?.ensureIndexVisible(currentPageStartIndex, 'top'); + } catch (err) { + console.log(err); + } + + setPagination({ + data: { + itemsPerPage: event.api.paginationGetPageSize(), + totalItems: event.api.paginationGetRowCount(), + totalPages: event.api.paginationGetTotalPages() + 1, + }, + }); + }, + [isPaginationEnabled, pagination.currentPage, pagination.itemsPerPage, setPagination], + ); + + const handleGridSizeChange = () => { + if (page.table.autoFit) { + tableRef?.current?.api.sizeColumnsToFit(); + } + }; + + const handleColumnChange = useCallback(() => { + const { columnApi } = tableRef?.current || {}; + const columnsOrder = columnApi?.getAllGridColumns(); + + if (!columnsOrder) return; + + const columnsInSettings = page.table.columns; + const updatedColumns = []; + for (const column of columnsOrder) { + const columnInSettings = columnsInSettings.find( + (c) => c.column === column.getColDef().colId, + ); + + if (columnInSettings) { + updatedColumns.push({ + ...columnInSettings, + ...(!page.table.autoFit && { + width: column.getActualWidth(), + }), + }); + } + } + + setTable({ columns: updatedColumns }); + }, [page.table.autoFit, page.table.columns, setTable, tableRef]); + + const debouncedColumnChange = debounce(handleColumnChange, 200); + + const handleScroll = (e: BodyScrollEvent) => { + const scrollOffset = Number((e.top / page.table.rowHeight).toFixed(0)); + setTable({ scrollOffset }); + }; + + const handleContextMenu = useHandleTableContextMenu( + LibraryItem.PLAYLIST, + PLAYLIST_CONTEXT_MENU_ITEMS, + ); + + const handleRowDoubleClick = (e: RowDoubleClickedEvent) => { + if (!e.data) return; + navigate(generatePath(AppRoute.PLAYLISTS_DETAIL, { playlistId: e.data.id })); + }; + + return ( + + + data.data.id} + infiniteInitialRowCount={itemCount || 100} + pagination={isPaginationEnabled} + paginationAutoPageSize={isPaginationEnabled} + paginationPageSize={page.table.pagination.itemsPerPage || 100} + rowBuffer={20} + rowHeight={page.table.rowHeight || 40} + rowModelType="infinite" + rowSelection="multiple" + onBodyScrollEnd={handleScroll} + onCellContextMenu={handleContextMenu} + onColumnMoved={handleColumnChange} + onColumnResized={debouncedColumnChange} + onGridReady={onGridReady} + onGridSizeChanged={handleGridSizeChange} + onPaginationChanged={onPaginationChanged} + onRowDoubleClicked={handleRowDoubleClick} + /> + + + {page.display === ListDisplayType.TABLE_PAGINATED && ( + + )} + + + ); +}; diff --git a/src/renderer/features/playlists/routes/playlist-list-route.tsx b/src/renderer/features/playlists/routes/playlist-list-route.tsx index 142713c6..86f0085b 100644 --- a/src/renderer/features/playlists/routes/playlist-list-route.tsx +++ b/src/renderer/features/playlists/routes/playlist-list-route.tsx @@ -1,6 +1,7 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { useRef } from 'react'; import { PlaylistListSort, SortOrder } from '/@/renderer/api/types'; +import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { PlaylistListContent } from '/@/renderer/features/playlists/components/playlist-list-content'; import { PlaylistListHeader } from '/@/renderer/features/playlists/components/playlist-list-header'; import { usePlaylistList } from '/@/renderer/features/playlists/queries/playlist-list-query'; @@ -8,6 +9,7 @@ import { AnimatedPage } from '/@/renderer/features/shared'; import { useCurrentServer } from '/@/renderer/store'; const PlaylistListRoute = () => { + const gridRef = useRef(null); const tableRef = useRef(null); const server = useCurrentServer(); @@ -33,10 +35,12 @@ const PlaylistListRoute = () => { return ( diff --git a/src/renderer/store/playlist.store.ts b/src/renderer/store/playlist.store.ts index afdeac3c..586af986 100644 --- a/src/renderer/store/playlist.store.ts +++ b/src/renderer/store/playlist.store.ts @@ -33,10 +33,16 @@ type DetailProps = { table: DetailTableProps; }; +type ListGridProps = { + itemsPerRow?: number; + scrollOffset?: number; +}; + export type PlaylistListFilter = Omit; interface PlaylistState { detail: DetailProps; + grid: ListGridProps; list: ListProps; } @@ -46,6 +52,7 @@ export interface PlaylistSlice extends PlaylistState { setDetailTable: (data: Partial) => void; setDetailTablePagination: (id: string, data: Partial) => void; setFilters: (data: Partial) => PlaylistListFilter; + setGrid: (args: { data: Partial }) => void; setStore: (data: Partial) => void; setTable: (data: Partial) => void; setTablePagination: (args: { data: Partial }) => void; @@ -90,6 +97,14 @@ export const usePlaylistStore = create()( return get().list.filter; }, + setGrid: (args) => { + set((state) => { + state.grid = { + ...state.grid, + ...args.data, + }; + }); + }, setStore: (data) => { set({ ...get(), ...data }); }, @@ -133,6 +148,10 @@ export const usePlaylistStore = create()( rowHeight: 60, }, }, + grid: { + itemsPerRow: 5, + scrollOffset: 0, + }, list: { display: ListDisplayType.TABLE, filter: { @@ -189,6 +208,8 @@ export const usePlaylistFilters = () => { return usePlaylistStore((state) => [state.list.filter, state.actions.setFilters]); }; +export const usePlaylistGridStore = () => usePlaylistStore((state) => state.grid); + export const usePlaylistListStore = () => usePlaylistStore((state) => state.list); export const usePlaylistTablePagination = () =>