diff --git a/src/renderer/features/albums/components/album-list-content.tsx b/src/renderer/features/albums/components/album-list-content.tsx index b0daa225..d3e13c19 100644 --- a/src/renderer/features/albums/components/album-list-content.tsx +++ b/src/renderer/features/albums/components/album-list-content.tsx @@ -1,43 +1,22 @@ -import { ALBUM_CARD_ROWS } from '/@/renderer/components'; -import { AppRoute } from '/@/renderer/router/routes'; -import { ListDisplayType, CardRow } from '/@/renderer/types'; -import AutoSizer from 'react-virtualized-auto-sizer'; -import { MutableRefObject, useCallback, useMemo } from 'react'; -import { ListOnScrollProps } from 'react-window'; -import { api } from '/@/renderer/api'; -import { controller } from '/@/renderer/api/controller'; -import { queryKeys } from '/@/renderer/api/query-keys'; -import { Album, AlbumListQuery, AlbumListSort, LibraryItem } from '/@/renderer/api/types'; -import { useQueryClient } from '@tanstack/react-query'; -import { - useCurrentServer, - useAlbumListStore, - useListStoreActions, - useAlbumListFilter, -} from '/@/renderer/store'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; -import { - BodyScrollEvent, - ColDef, - GridReadyEvent, - IDatasource, - PaginationChangedEvent, - RowDoubleClickedEvent, -} from '@ag-grid-community/core'; -import { AnimatePresence } from 'framer-motion'; -import debounce from 'lodash/debounce'; -import { useHandleTableContextMenu } from '/@/renderer/features/context-menu'; -import { ALBUM_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; -import { generatePath, useNavigate } from 'react-router'; -import { usePlayQueueAdd } from '/@/renderer/features/player'; -import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared'; +import { lazy, MutableRefObject, Suspense } from 'react'; +import { Spinner } from '/@/renderer/components'; +import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; -import { - VirtualInfiniteGridRef, - VirtualGridAutoSizerContainer, - VirtualInfiniteGrid, -} from '/@/renderer/components/virtual-grid'; -import { getColumnDefs, VirtualTable, TablePagination } from '/@/renderer/components/virtual-table'; +import { useAlbumListStore } from '/@/renderer/store'; +import { ListDisplayType } from '/@/renderer/types'; + +const AlbumListGridView = lazy(() => + import('/@/renderer/features/albums/components/album-list-grid-view').then((module) => ({ + default: module.AlbumListGridView, + })), +); + +const AlbumListTableView = lazy(() => + import('/@/renderer/features/albums/components/album-list-table-view').then((module) => ({ + default: module.AlbumListTableView, + })), +); interface AlbumListContentProps { gridRef: MutableRefObject; @@ -46,348 +25,22 @@ interface AlbumListContentProps { } export const AlbumListContent = ({ itemCount, gridRef, tableRef }: AlbumListContentProps) => { - const queryClient = useQueryClient(); - const navigate = useNavigate(); - const server = useCurrentServer(); - const handlePlayQueueAdd = usePlayQueueAdd(); const { id, pageKey } = useAlbumListContext(); - const filter = useAlbumListFilter({ id, key: pageKey }); - const { setTable, setTablePagination, setGrid } = useListStoreActions(); - const { table, grid, display } = useAlbumListStore({ id, key: pageKey }); - const isPaginationEnabled = display === ListDisplayType.TABLE_PAGINATED; - - const columnDefs: ColDef[] = useMemo(() => getColumnDefs(table.columns), [table.columns]); - - const onTableReady = useCallback( - (params: GridReadyEvent) => { - const dataSource: IDatasource = { - getRows: async (params) => { - const limit = params.endRow - params.startRow; - const startIndex = params.startRow; - - const query: AlbumListQuery = { - limit, - startIndex, - ...filter, - _custom: { - jellyfin: { - ...filter._custom?.jellyfin, - }, - navidrome: { - ...filter._custom?.navidrome, - }, - }, - }; - - const queryKey = queryKeys.albums.list(server?.id || '', query); - - if (!server) { - return params.failCallback(); - } - - const albumsRes = await queryClient.fetchQuery( - queryKey, - async ({ signal }) => - api.controller.getAlbumList({ - apiClientProps: { - server, - signal, - }, - query, - }), - { cacheTime: 1000 * 60 * 1 }, - ); - - return params.successCallback(albumsRes?.items || [], albumsRes?.totalRecordCount || 0); - }, - rowCount: undefined, - }; - params.api.setDatasource(dataSource); - params.api.ensureIndexVisible(table.scrollOffset || 0, 'top'); - }, - [filter, queryClient, server, table.scrollOffset], - ); - - const onTablePaginationChanged = useCallback( - (event: PaginationChangedEvent) => { - if (!isPaginationEnabled || !event.api) return; - - try { - // Scroll to top of page on pagination change - const currentPageStartIndex = table.pagination.currentPage * table.pagination.itemsPerPage; - event.api?.ensureIndexVisible(currentPageStartIndex, 'top'); - } catch (err) { - console.log(err); - } - - setTablePagination({ - data: { - itemsPerPage: event.api.paginationGetPageSize(), - totalItems: event.api.paginationGetRowCount(), - totalPages: event.api.paginationGetTotalPages() + 1, - }, - key: pageKey, - }); - }, - [ - isPaginationEnabled, - setTablePagination, - pageKey, - table.pagination.currentPage, - table.pagination.itemsPerPage, - ], - ); - - const handleTableColumnChange = useCallback(() => { - const { columnApi } = tableRef?.current || {}; - const columnsOrder = columnApi?.getAllGridColumns(); - - if (!columnsOrder) return; - - const columnsInSettings = table.columns; - const updatedColumns = []; - for (const column of columnsOrder) { - const columnInSettings = columnsInSettings.find((c) => c.column === column.getColDef().colId); - - if (columnInSettings) { - updatedColumns.push({ - ...columnInSettings, - ...(!table.autoFit && { - width: column.getColDef().width, - }), - }); - } - } - - setTable({ data: { columns: updatedColumns }, key: pageKey }); - }, [tableRef, table.columns, table.autoFit, setTable, pageKey]); - - const debouncedTableColumnChange = debounce(handleTableColumnChange, 200); - - const handleTableScroll = (e: BodyScrollEvent) => { - const scrollOffset = Number((e.top / table.rowHeight).toFixed(0)); - setTable({ data: { scrollOffset }, key: pageKey }); - }; - - const fetch = useCallback( - async ({ skip, take }: { skip: number; take: number }) => { - if (!server) { - return []; - } - - const query: AlbumListQuery = { - limit: take, - startIndex: skip, - ...filter, - _custom: { - jellyfin: { - ...filter._custom?.jellyfin, - }, - navidrome: { - ...filter._custom?.navidrome, - }, - }, - }; - - const queryKey = queryKeys.albums.list(server?.id || '', query); - - const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) => - controller.getAlbumList({ - apiClientProps: { - server, - signal, - }, - query, - }), - ); - - return albums; - }, - [filter, queryClient, server], - ); - - const handleGridScroll = useCallback( - (e: ListOnScrollProps) => { - setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey }); - }, - [pageKey, setGrid], - ); - - const cardRows = useMemo(() => { - const rows: CardRow[] = [ALBUM_CARD_ROWS.name]; - - switch (filter.sortBy) { - case AlbumListSort.ALBUM_ARTIST: - rows.push(ALBUM_CARD_ROWS.albumArtists); - rows.push(ALBUM_CARD_ROWS.releaseYear); - break; - case AlbumListSort.ARTIST: - rows.push(ALBUM_CARD_ROWS.artists); - rows.push(ALBUM_CARD_ROWS.releaseYear); - break; - case AlbumListSort.COMMUNITY_RATING: - rows.push(ALBUM_CARD_ROWS.albumArtists); - break; - case AlbumListSort.DURATION: - rows.push(ALBUM_CARD_ROWS.albumArtists); - rows.push(ALBUM_CARD_ROWS.duration); - break; - case AlbumListSort.FAVORITED: - rows.push(ALBUM_CARD_ROWS.albumArtists); - rows.push(ALBUM_CARD_ROWS.releaseYear); - break; - case AlbumListSort.NAME: - rows.push(ALBUM_CARD_ROWS.albumArtists); - rows.push(ALBUM_CARD_ROWS.releaseYear); - break; - case AlbumListSort.PLAY_COUNT: - rows.push(ALBUM_CARD_ROWS.albumArtists); - rows.push(ALBUM_CARD_ROWS.playCount); - break; - case AlbumListSort.RANDOM: - rows.push(ALBUM_CARD_ROWS.albumArtists); - rows.push(ALBUM_CARD_ROWS.releaseYear); - break; - case AlbumListSort.RATING: - rows.push(ALBUM_CARD_ROWS.albumArtists); - rows.push(ALBUM_CARD_ROWS.rating); - break; - case AlbumListSort.RECENTLY_ADDED: - rows.push(ALBUM_CARD_ROWS.albumArtists); - rows.push(ALBUM_CARD_ROWS.createdAt); - break; - case AlbumListSort.RECENTLY_PLAYED: - rows.push(ALBUM_CARD_ROWS.albumArtists); - rows.push(ALBUM_CARD_ROWS.lastPlayedAt); - break; - case AlbumListSort.SONG_COUNT: - rows.push(ALBUM_CARD_ROWS.albumArtists); - rows.push(ALBUM_CARD_ROWS.songCount); - break; - case AlbumListSort.YEAR: - rows.push(ALBUM_CARD_ROWS.albumArtists); - rows.push(ALBUM_CARD_ROWS.releaseYear); - break; - case AlbumListSort.RELEASE_DATE: - rows.push(ALBUM_CARD_ROWS.albumArtists); - rows.push(ALBUM_CARD_ROWS.releaseDate); - } - - return rows; - }, [filter.sortBy]); - - const handleContextMenu = useHandleTableContextMenu(LibraryItem.ALBUM, ALBUM_CONTEXT_MENU_ITEMS); - - const handleRowDoubleClick = (e: RowDoubleClickedEvent) => { - navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id })); - }; - - 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 { display } = useAlbumListStore({ id, key: pageKey }); return ( - <> - - {display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? ( - - {({ height, width }) => ( - <> - - - )} - - ) : ( - data.data.id} - infiniteInitialRowCount={itemCount || 100} - pagination={isPaginationEnabled} - paginationAutoPageSize={isPaginationEnabled} - paginationPageSize={table.pagination.itemsPerPage || 100} - rowBuffer={20} - rowHeight={table.rowHeight || 40} - rowModelType="infinite" - onBodyScrollEnd={handleTableScroll} - onCellContextMenu={handleContextMenu} - onColumnMoved={handleTableColumnChange} - onColumnResized={debouncedTableColumnChange} - onGridReady={onTableReady} - onPaginationChanged={onTablePaginationChanged} - onRowDoubleClicked={handleRowDoubleClick} - /> - )} - - {isPaginationEnabled && ( - - {display === ListDisplayType.TABLE_PAGINATED && ( - - )} - + }> + {display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? ( + + ) : ( + )} - + ); }; diff --git a/src/renderer/features/albums/components/album-list-grid-view.tsx b/src/renderer/features/albums/components/album-list-grid-view.tsx new file mode 100644 index 00000000..57adb16f --- /dev/null +++ b/src/renderer/features/albums/components/album-list-grid-view.tsx @@ -0,0 +1,200 @@ +import { useCallback, useMemo } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import { ListOnScrollProps } from 'react-window'; +import { controller } from '/@/renderer/api/controller'; +import { queryKeys } from '/@/renderer/api/query-keys'; +import { Album, AlbumListQuery, AlbumListSort, LibraryItem } from '/@/renderer/api/types'; +import { ALBUM_CARD_ROWS } from '/@/renderer/components'; +import { + VirtualGridAutoSizerContainer, + VirtualInfiniteGrid, +} from '/@/renderer/components/virtual-grid'; +import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; +import { usePlayQueueAdd } from '/@/renderer/features/player'; +import { AppRoute } from '/@/renderer/router/routes'; +import { + useAlbumListFilter, + useAlbumListStore, + useCurrentServer, + useListStoreActions, +} from '/@/renderer/store'; +import { CardRow, ListDisplayType } from '/@/renderer/types'; +import { useCreateFavorite, useDeleteFavorite } from '/@/renderer/features/shared'; + +export const AlbumListGridView = ({ gridRef, itemCount }: any) => { + const queryClient = useQueryClient(); + const server = useCurrentServer(); + const handlePlayQueueAdd = usePlayQueueAdd(); + const { id, pageKey } = useAlbumListContext(); + const { grid, display } = useAlbumListStore({ id, key: pageKey }); + const { setGrid } = useListStoreActions(); + const filter = useAlbumListFilter({ id, key: pageKey }); + + 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[] = [ALBUM_CARD_ROWS.name]; + + switch (filter.sortBy) { + case AlbumListSort.ALBUM_ARTIST: + rows.push(ALBUM_CARD_ROWS.albumArtists); + rows.push(ALBUM_CARD_ROWS.releaseYear); + break; + case AlbumListSort.ARTIST: + rows.push(ALBUM_CARD_ROWS.artists); + rows.push(ALBUM_CARD_ROWS.releaseYear); + break; + case AlbumListSort.COMMUNITY_RATING: + rows.push(ALBUM_CARD_ROWS.albumArtists); + break; + case AlbumListSort.DURATION: + rows.push(ALBUM_CARD_ROWS.albumArtists); + rows.push(ALBUM_CARD_ROWS.duration); + break; + case AlbumListSort.FAVORITED: + rows.push(ALBUM_CARD_ROWS.albumArtists); + rows.push(ALBUM_CARD_ROWS.releaseYear); + break; + case AlbumListSort.NAME: + rows.push(ALBUM_CARD_ROWS.albumArtists); + rows.push(ALBUM_CARD_ROWS.releaseYear); + break; + case AlbumListSort.PLAY_COUNT: + rows.push(ALBUM_CARD_ROWS.albumArtists); + rows.push(ALBUM_CARD_ROWS.playCount); + break; + case AlbumListSort.RANDOM: + rows.push(ALBUM_CARD_ROWS.albumArtists); + rows.push(ALBUM_CARD_ROWS.releaseYear); + break; + case AlbumListSort.RATING: + rows.push(ALBUM_CARD_ROWS.albumArtists); + rows.push(ALBUM_CARD_ROWS.rating); + break; + case AlbumListSort.RECENTLY_ADDED: + rows.push(ALBUM_CARD_ROWS.albumArtists); + rows.push(ALBUM_CARD_ROWS.createdAt); + break; + case AlbumListSort.RECENTLY_PLAYED: + rows.push(ALBUM_CARD_ROWS.albumArtists); + rows.push(ALBUM_CARD_ROWS.lastPlayedAt); + break; + case AlbumListSort.SONG_COUNT: + rows.push(ALBUM_CARD_ROWS.albumArtists); + rows.push(ALBUM_CARD_ROWS.songCount); + break; + case AlbumListSort.YEAR: + rows.push(ALBUM_CARD_ROWS.albumArtists); + rows.push(ALBUM_CARD_ROWS.releaseYear); + break; + case AlbumListSort.RELEASE_DATE: + rows.push(ALBUM_CARD_ROWS.albumArtists); + rows.push(ALBUM_CARD_ROWS.releaseDate); + } + + return rows; + }, [filter.sortBy]); + + const handleGridScroll = useCallback( + (e: ListOnScrollProps) => { + setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey }); + }, + [pageKey, setGrid], + ); + + const fetch = useCallback( + async ({ skip, take }: { skip: number; take: number }) => { + if (!server) { + return []; + } + + const query: AlbumListQuery = { + limit: take, + startIndex: skip, + ...filter, + _custom: { + jellyfin: { + ...filter._custom?.jellyfin, + }, + navidrome: { + ...filter._custom?.navidrome, + }, + }, + }; + + const queryKey = queryKeys.albums.list(server?.id || '', query); + + const albums = await queryClient.fetchQuery(queryKey, async ({ signal }) => + controller.getAlbumList({ + apiClientProps: { + server, + signal, + }, + query, + }), + ); + + return albums; + }, + [filter, queryClient, server], + ); + + return ( + + + {({ height, width }) => ( + + )} + + + ); +}; diff --git a/src/renderer/features/albums/components/album-list-table-view.tsx b/src/renderer/features/albums/components/album-list-table-view.tsx new file mode 100644 index 00000000..906f36b0 --- /dev/null +++ b/src/renderer/features/albums/components/album-list-table-view.tsx @@ -0,0 +1,207 @@ +import { useMemo, useCallback } from 'react'; +import { + ColDef, + GridReadyEvent, + IDatasource, + PaginationChangedEvent, + BodyScrollEvent, + RowDoubleClickedEvent, +} from '@ag-grid-community/core'; +import { api } from '/@/renderer/api'; +import { queryKeys } from '/@/renderer/api/query-keys'; +import { AlbumListQuery, LibraryItem } from '/@/renderer/api/types'; +import { getColumnDefs, TablePagination, VirtualTable } from '/@/renderer/components/virtual-table'; +import { useAlbumListContext } from '/@/renderer/features/albums/context/album-list-context'; +import { + useCurrentServer, + useAlbumListFilter, + useListStoreActions, + useAlbumListStore, +} from '/@/renderer/store'; +import { useQueryClient } from '@tanstack/react-query'; +import { AnimatePresence } from 'framer-motion'; +import debounce from 'lodash/debounce'; +import { ListDisplayType } from '/@/renderer/types'; +import { generatePath, useNavigate } from 'react-router'; +import { useHandleTableContextMenu } from '/@/renderer/features/context-menu'; +import { ALBUM_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; +import { AppRoute } from '/@/renderer/router/routes'; +import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid'; + +export const AlbumListTableView = ({ tableRef, itemCount }: any) => { + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const server = useCurrentServer(); + const { id, pageKey } = useAlbumListContext(); + const filter = useAlbumListFilter({ id, key: pageKey }); + const { setTable, setTablePagination } = useListStoreActions(); + const { table, display } = useAlbumListStore({ id, key: pageKey }); + const columnDefs: ColDef[] = useMemo(() => getColumnDefs(table.columns), [table.columns]); + const isPaginationEnabled = display === ListDisplayType.TABLE_PAGINATED; + + const onTableReady = useCallback( + (params: GridReadyEvent) => { + const dataSource: IDatasource = { + getRows: async (params) => { + const limit = params.endRow - params.startRow; + const startIndex = params.startRow; + + const query: AlbumListQuery = { + limit, + startIndex, + ...filter, + _custom: { + jellyfin: { + ...filter._custom?.jellyfin, + }, + navidrome: { + ...filter._custom?.navidrome, + }, + }, + }; + + const queryKey = queryKeys.albums.list(server?.id || '', query); + + if (!server) { + return params.failCallback(); + } + + const albumsRes = await queryClient.fetchQuery( + queryKey, + async ({ signal }) => + api.controller.getAlbumList({ + apiClientProps: { + server, + signal, + }, + query, + }), + { cacheTime: 1000 * 60 * 1 }, + ); + + return params.successCallback(albumsRes?.items || [], albumsRes?.totalRecordCount || 0); + }, + rowCount: undefined, + }; + params.api.setDatasource(dataSource); + params.api.ensureIndexVisible(table.scrollOffset || 0, 'top'); + }, + [filter, queryClient, server, table.scrollOffset], + ); + + const onTablePaginationChanged = useCallback( + (event: PaginationChangedEvent) => { + if (!isPaginationEnabled || !event.api) return; + + try { + // Scroll to top of page on pagination change + const currentPageStartIndex = table.pagination.currentPage * table.pagination.itemsPerPage; + event.api?.ensureIndexVisible(currentPageStartIndex, 'top'); + } catch (err) { + console.log(err); + } + + setTablePagination({ + data: { + itemsPerPage: event.api.paginationGetPageSize(), + totalItems: event.api.paginationGetRowCount(), + totalPages: event.api.paginationGetTotalPages() + 1, + }, + key: pageKey, + }); + }, + [ + isPaginationEnabled, + setTablePagination, + pageKey, + table.pagination.currentPage, + table.pagination.itemsPerPage, + ], + ); + + const handleTableColumnChange = useCallback(() => { + const { columnApi } = tableRef?.current || {}; + const columnsOrder = columnApi?.getAllGridColumns(); + + if (!columnsOrder) return; + + const columnsInSettings = table.columns; + const updatedColumns = []; + for (const column of columnsOrder) { + const columnInSettings = columnsInSettings.find((c) => c.column === column.getColDef().colId); + + if (columnInSettings) { + updatedColumns.push({ + ...columnInSettings, + ...(!table.autoFit && { + width: column.getColDef().width, + }), + }); + } + } + + setTable({ data: { columns: updatedColumns }, key: pageKey }); + }, [tableRef, table.columns, table.autoFit, setTable, pageKey]); + + const debouncedTableColumnChange = debounce(handleTableColumnChange, 200); + + const handleTableScroll = (e: BodyScrollEvent) => { + const scrollOffset = Number((e.top / table.rowHeight).toFixed(0)); + setTable({ data: { scrollOffset }, key: pageKey }); + }; + + const handleContextMenu = useHandleTableContextMenu(LibraryItem.ALBUM, ALBUM_CONTEXT_MENU_ITEMS); + + const handleRowDoubleClick = (e: RowDoubleClickedEvent) => { + navigate(generatePath(AppRoute.LIBRARY_ALBUMS_DETAIL, { albumId: e.data.id })); + }; + + return ( + <> + + data.data.id} + infiniteInitialRowCount={itemCount || 100} + pagination={isPaginationEnabled} + paginationAutoPageSize={isPaginationEnabled} + paginationPageSize={table.pagination.itemsPerPage || 100} + rowBuffer={20} + rowHeight={table.rowHeight || 40} + rowModelType="infinite" + onBodyScrollEnd={handleTableScroll} + onCellContextMenu={handleContextMenu} + onColumnMoved={handleTableColumnChange} + onColumnResized={debouncedTableColumnChange} + onGridReady={onTableReady} + onPaginationChanged={onTablePaginationChanged} + onRowDoubleClicked={handleRowDoubleClick} + /> + + {isPaginationEnabled && ( + + {display === ListDisplayType.TABLE_PAGINATED && ( + + )} + + )} + + ); +};