diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index 95a4b675..d61460b0 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -402,7 +402,7 @@ export type AlbumDetailQuery = { id: string }; export type AlbumDetailArgs = { query: AlbumDetailQuery } & BaseEndpointArgs; // Song List -export type SongListResponse = BasePaginatedResponse; +export type SongListResponse = BasePaginatedResponse | null | undefined; export enum SongListSort { ALBUM = 'album', diff --git a/src/renderer/features/songs/components/song-list-content.tsx b/src/renderer/features/songs/components/song-list-content.tsx index 8ec5a5f5..f6de2f9e 100644 --- a/src/renderer/features/songs/components/song-list-content.tsx +++ b/src/renderer/features/songs/components/song-list-content.tsx @@ -1,33 +1,15 @@ -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, - useListStoreActions, - useSongListFilter, - useSongListStore, -} 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 { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; -import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; -import { LibraryItem, QueueSong, SongListQuery } from '/@/renderer/api/types'; +import { lazy, MutableRefObject, Suspense } from 'react'; +import { Spinner } from '/@/renderer/components'; import { useSongListContext } from '/@/renderer/features/songs/context/song-list-context'; -import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid'; -import { getColumnDefs, VirtualTable, TablePagination } from '/@/renderer/components/virtual-table'; +import { useSongListStore } from '/@/renderer/store'; +import { ListDisplayType } from '/@/renderer/types'; + +const SongListTableView = lazy(() => + import('/@/renderer/features/songs/components/song-list-table-view').then((module) => ({ + default: module.SongListTableView, + })), +); interface SongListContentProps { itemCount?: number; @@ -35,184 +17,21 @@ interface SongListContentProps { } export const SongListContent = ({ itemCount, tableRef }: SongListContentProps) => { - const queryClient = useQueryClient(); - const server = useCurrentServer(); + const { id, pageKey } = useSongListContext(); + const { display } = useSongListStore({ id, key: pageKey }); - const { id, pageKey, handlePlay } = useSongListContext(); - const filter = useSongListFilter({ id, key: pageKey }); - const { display, table } = useSongListStore({ id, key: pageKey }); - - const { setTable, setTablePagination } = useListStoreActions(); - const playButtonBehavior = usePlayButtonBehavior(); - - const isPaginationEnabled = display === ListDisplayType.TABLE_PAGINATED; - - const columnDefs: ColDef[] = useMemo(() => getColumnDefs(table.columns), [table.columns]); - - const onGridReady = useCallback( - (params: GridReadyEvent) => { - const dataSource: IDatasource = { - getRows: async (params) => { - const limit = params.endRow - params.startRow; - const startIndex = params.startRow; - - const query: SongListQuery = { - limit, - startIndex, - ...filter, - }; - - const queryKey = queryKeys.songs.list(server?.id || '', query); - - const songsRes = await queryClient.fetchQuery( - queryKey, - async ({ signal }) => - api.controller.getSongList({ - apiClientProps: { - server, - signal, - }, - query, - }), - { cacheTime: 1000 * 60 * 1 }, - ); - - params.successCallback(songsRes?.items || [], songsRes?.totalRecordCount || 0); - }, - rowCount: undefined, - }; - params.api.setDatasource(dataSource); - - params.api.ensureIndexVisible(table.scrollOffset, 'top'); - }, - [filter, 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 = - 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, - pageKey, - setTablePagination, - table.pagination.currentPage, - table.pagination.itemsPerPage, - ], - ); - - const handleGridSizeChange = () => { - if (table.autoFit) { - tableRef?.current?.api.sizeColumnsToFit(); - } - }; - - const handleColumnChange = 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.getActualWidth(), - }), - }); - } - } - - setTable({ data: { columns: updatedColumns }, key: pageKey }); - }, [tableRef, table.columns, table.autoFit, setTable, pageKey]); - - const debouncedColumnChange = debounce(handleColumnChange, 200); - - const handleScroll = (e: BodyScrollEvent) => { - const scrollOffset = Number((e.top / table.rowHeight).toFixed(0)); - setTable({ data: { scrollOffset }, key: pageKey }); - }; - - const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, SONG_CONTEXT_MENU_ITEMS); - - const handleRowDoubleClick = (e: RowDoubleClickedEvent) => { - if (!e.data) return; - handlePlay?.({ initialSongId: e.data.id, playType: playButtonBehavior }); - }; + const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER; return ( - - - data.data.id} - infiniteInitialRowCount={itemCount || 100} - pagination={isPaginationEnabled} - paginationAutoPageSize={isPaginationEnabled} - paginationPageSize={table.pagination.itemsPerPage || 100} - rowBuffer={20} - rowHeight={table.rowHeight || 40} - rowModelType="infinite" - rowSelection="multiple" - onBodyScrollEnd={handleScroll} - onCellContextMenu={handleContextMenu} - onColumnMoved={handleColumnChange} - onColumnResized={debouncedColumnChange} - onGridReady={onGridReady} - onGridSizeChanged={handleGridSizeChange} - onPaginationChanged={onPaginationChanged} - onRowDoubleClicked={handleRowDoubleClick} + }> + {isGrid ? ( + <> + ) : ( + - - - {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 7ac3ce4e..b2623f9a 100644 --- a/src/renderer/features/songs/components/song-list-header-filters.tsx +++ b/src/renderer/features/songs/components/song-list-header-filters.tsx @@ -1,11 +1,9 @@ import { useCallback, useMemo, ChangeEvent, MutableRefObject, MouseEvent } from 'react'; import { IDatasource } from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; -import { Flex, Group, Stack } from '@mantine/core'; +import { Divider, Flex, Group, Stack } from '@mantine/core'; import { openModal } from '@mantine/modals'; import { - RiSortAsc, - RiSortDesc, RiFolder2Line, RiMoreFill, RiSettings3Fill, @@ -19,7 +17,7 @@ import { api } from '/@/renderer/api'; import { queryKeys } from '/@/renderer/api/query-keys'; import { LibraryItem, SongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types'; import { DropdownMenu, Button, Slider, MultiSelect, Switch, Text } from '/@/renderer/components'; -import { useMusicFolders } from '/@/renderer/features/shared'; +import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared'; import { JellyfinSongFilters } from '/@/renderer/features/songs/components/jellyfin-song-filters'; import { NavidromeSongFilters } from '/@/renderer/features/songs/components/navidrome-song-filters'; import { useContainerQuery } from '/@/renderer/hooks'; @@ -79,11 +77,6 @@ const FILTERS = { ], }; -const ORDER = [ - { name: 'Ascending', value: SortOrder.ASC }, - { name: 'Descending', value: SortOrder.DESC }, -]; - interface SongListHeaderFiltersProps { tableRef: MutableRefObject; } @@ -106,8 +99,6 @@ export const SongListHeaderFilters = ({ tableRef }: SongListHeaderFiltersProps) ).find((f) => f.value === filter.sortBy)?.name) || 'Unknown'; - const sortOrderLabel = ORDER.find((s) => s.value === filter.sortOrder)?.name; - const handleFilterChange = useCallback( async (filters?: SongListFilter) => { const dataSource: IDatasource = { @@ -340,51 +331,56 @@ export const SongListHeaderFilters = ({ tableRef }: SongListHeaderFiltersProps) ))} + + + {server?.type === ServerType.JELLYFIN && ( + <> + + + + + + + {musicFoldersQuery.data?.items.map((folder) => ( + + {folder.name} + + ))} + + + + )} + - {server?.type === ServerType.JELLYFIN && ( - - - - - - {musicFoldersQuery.data?.items.map((folder) => ( - - {folder.name} - - ))} - - - )} +